Repository navigation
fix(cpu): a grow runs a fork-free task in place when a fork follows it (#1475) - #1526
Merged
Merged
Conversation
bendlang#1475) A non-flat call before a fork capped scaling. A grow pushed every fork-free task it met back for the drain, even one whose continuation forks, so the drain ran that fork and its whole subtree on one thread. monk_step now runs such a task in place on the host and keeps the row's stride, so the fork deals within the row, as inlined code does. A task whose continuation is a join (resw 0), a fork-free frame or the root still waits for the drain, and the device schedule is unchanged. bendlang#1475's tree with a non-flat spin (8 levels, 2M leaf steps), 16 threads on an M3 Max, median of 3: K = 0 0.48 -> 0.09 s, K = 2M 0.94 -> 0.19 s (1 thread: 0.94 and 1.87 s). The flat tree is unchanged. Of the 17 runtime benches only tree-bitonic reaches the path (100 times at 4 threads, 242 at 16); every output matches its pin, and all 17 are within 2% of main at 16 threads.
Lorenzobattistela
approved these changes
Oct 13, 2026
Contributor
|
on bend-bench this PR screens clean: it emits code that passes every test the base passes, compiles at the same speed, and shows no real slowdown across the CPU and GPU workloads. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows #1479, which fixed #1475 for flat code and left one case open. If the work before each fork is a non-flat call (one that isn't a tail loop, so it isn't inlined as a
spin_N), a fork tree still got about 2x from 16 threads, even when that call does nothing. #1479 put that down to the non-flat leaves, which was wrong: the leaves scale fine. What caps it is the call before the fork.The cause
./nonflat 8 2000000 0 --threads T --gpu offis #1475's tree with a non-flatspin. At K = 0, the callspin(0, i)before each fork returns at once. M3 Max, median of 3:spinat the leaves onlyspin(0, i)before each forkRemoving the empty call removes the cap. Here is how the call stops the grow:
emit_jumpdoesn't run a fork-free (nofk) callee inline. It returns it to the caller as a task (!DEVICE && !seq && fid_nofk(fid)), and the continuation K after the call waits on it.monk_stepis growing (stride != 0), it pushes any ready nofk task back onto a ring for the drain and ends the pass. That's right for a call that a join follows. It's wrong for a call that a fork follows, because the grow never reaches that fork.FID_EXITkeeps it sequential for the K. So the fork after the call, and every level below it, runs on one thread's stack.spin, so nothing grows. The drain then holds 2 tasks, each a 128-leaf subtree, on 2 threads, and the other 14 sit idle. Flatspinis inlined, so there's no handoff, and the grow runs it and reaches the fork.What changes
monk_stepruns a ready nofk task in place, inside the grow, when its continuation can fork:Each condition has a reason:
fid_resw(k) != 0picks out a K: a segment with a frame, whose results arrive in registers. A join has no frame, so itsreswis 0. Without this check, a leaf that tail-calls a nofk function into a join that can fork would run leaf work inside the grow.!fid_nofk(k)means the code after the call can fork. A task followed by a fork-free K stays deferred, as before.k == TERM_HOLEguards the root. Itsterm_auxis 0xFFFF, which would index pastFID_T.FID_EXIThas the same guard.DEVICE ||keeps the GPU schedule exactly as it was. The device never hands off calls.H_CURSORinstead.The ring-head skip doesn't change. No nofk task whose K can fork reaches a ring head: the grow no longer pushes one, and
task_dealpushes only fork kids, which a join follows.The risk is the same one inlined code already carries. If the K forks in only some branches (
x = heavy(d); match d: 0 → x; _ → fork), a grow runsheavyat the leaves too.comp.tsgoes from 63,067 to 63,147 tokens.Results
M3 Max,
--gpu off, median of 3. Both builds print the same output in every row.The flat tree from #1475 (
spread 8 20000000 K) is unchanged, at K = 0 and K = 20M, on 4 and 16 threads.Test
tests/run/fork_after_call.bendcovers both shapes that reach the new path. An instrumented build confirmed each:spinbeforespread's fork: reached 254 times at 4 and at 16 threads.pair's join, which tail-callsspinand returns intochain's fork: reached once.pair's kids callleaf, which can fork, so the grow runs them and reaches the join. Fork-free kids would be left for the drain, and the join would never be reached in the grow. The test prints417464320and2860191008in the interpreter, under bun, and in C at 1, 4 and 16 threads, on main and on this branch.Benches (local A/B)
warp's join, which tail-callswarp_zipand is continued bybsort's K intoflow. Before, the drain ran each suchflowsubtree on one thread.What I ran, and what I couldn't
gates/test.ts's shard script, run locally on this Mac in 4 shards, passes 1605 / 1607. The two failures aregfx_clicksandgfx_window. They expect "no display", but this Mac has a desktop session, so a window opens. main's build gives the same output here.gates/repo.tspasses, 54 / 54.tscreports no new errors.DEVICE ||reduces the device's condition to the old one, so its schedule doesn't change.