Repository navigation
nemotron metal: a GPU round prices its head depth from the window-cost table - #1115
Open
jishnuvenugopal wants to merge 1 commit into
Open
jishnuvenugopal wants to merge 1 commit into
jishnuvenugopal wants to merge 1 commit into
Conversation
…t table round_depth chose between all of the head's levels and a fixed 4, a rule that treats a verify window of up to 5 rows as costing about one row. The backend's window-cost table says otherwise on a chip whose row projection runs per row (M1 to M4): there a deeper window costs more than the tokens it adds. Depth.init now prices each depth s as costs.at(s + 1) + s * head_ms and starts at the depth with the most expected tokens per ms under a 0.75 per-level survival prior. Every 8 rounds Depth.next compares the realized tokens per ms of each depth up to the current one, and steps one level deeper only when the current depth's full windows would pay for the extra row.
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.
Thank you, @ashhart, for the GPU round and the head-depth rule in
round_depth.zigthat this refines, and for thewindow-cost table the Metal backend already measures at load (
copy_lanes.Costs): it is what lets the depth be pricedinstead of fixed.
What this changes
round_depth.Depth(zig/src/families/nemotron/round_depth.zig) started a GPU round at all8levels and dropped to afixed
4when the levels past 4 brought under a tenth of the tokens. That treats a verify window of up to 5 rows ascosting about one row, which the backend's window-cost table contradicts on a chip whose row projections run without
tensor units (
zig/kernels/metal/core/row_projection.metal): on an M4 each row adds 7 to 9 ms to a 20 ms window.Depth.initnow takes the table and the head step time fromgpu_round.runand prices each depthsascosts.at(s + 1) + s * head_ms. It starts at the depth with the most expected tokens per ms under a 0.75 per-levelsurvival prior, or at
min(4, most)when the table is empty. Every 8 roundsDepth.nextcompares the realized tokensper ms of each depth up to the current one, and goes one level deeper only when the current depth's full windows would
pay for the extra row. A fixed depth is unchanged.
round_depth.zigjoinsnemotron.zig's test block for its two newtests.
This is the depth of one stream's GPU round on Metal. It does not touch the depth rule of shared rounds, where several
streams verify together, which #1059 is about (thanks, @plotarmordev).
Verified outcome
essay-style text it was 9% slower (42.8 against 47.2 tok/s). A round that drafted 4 or 8 levels deep paid for verify
rows whose tokens were mostly thrown away.
goes deeper only when the deeper rows keep paying for themselves.
tensorfold run(51.1to 60.1 tok/s on code, 42.8 to 54.3 on essay text) and 16% to 26% faster under
serve, 26% to 33% faster onhigh-acceptance copy prompts (78.6 to 99.0 tok/s copying a paragraph, 81.5 to 108.3 copying an 8K list), and now 15%
to 139% above plain decoding on every prompt measured. Every reply is token for token the same as before. Four
requests at once, prompt speed and memory are unchanged. Not measured: any chip with tensor units (M5), where this
changes the depth a round starts at.
Receipt
Environment: branch
perf/nemotron-round-depth-costsonmainat d1018b3 (1.1.1), Zig 0.17.0, Apple M4 (32 GB),macOS 27.0.1,
TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bitat d9d758fb. Load averages 1.4 to 2.9 (noother workload), recorded per run.
Exactness:
tensorfold run, 256 tokens, temperature 0, four prompts (fibonacci-raw, gpu-chat-no-think,sql-chat-no-think, essay-chat-no-think). Each prompt's plain run (
--no-drafts), fixed depths 1 to 4, and two runs ofmain and two of this branch at
--gpu-round 8: all 8 drafted runs per prompt equal the plain run's 256 tokens (32 of32:
54195f74313a,dc6df8c9622e,bdba18f79184,498e92e027a3).serve:tools/bench_openai.pytoken_shac91869ce1808and5ef050084ac3on all four launches,tools/bench_concurrent.py --alone --serial0 unequal, 0 failedon all four, andbench/d4_copy.pytext_sha8d5581a06df6,fd621d8d4fc4,dcd1880a0a4f(server token_sha5414bb3db7e3,15045d7e74d1,eba0eb1969af) equal across all 16 drafted and plain runs per prompt.Decode before and after,
tensorfold run --max-tokens 256 --warmup, one window-cost table timed by the first draftedrun and kept by every later run of both builds (1 to 5 rows: 19.74, 27.22, 35.96, 43.36, 52.50 ms; head step 1.505
ms), tok/s (the depth the rule chose, in rounds):
The rule picks a depth from the table and the prompt's own acceptance; on fibonacci-raw a fixed depth 3 is faster
still (64.2) and a fixed depth 1 is 0.3 to 1.4 tok/s ahead on the other three, so a better prior is possible, but no
single fixed depth is best on all four prompts, and a fixed 1 would be wrong on a chip whose table is flat.
Decode before and after,
serve --parallel 4 --context 34816 --prompt-cache-gib 0, four alternated launches (thisbranch, main, this branch, main) on one shared window-cost table timed by main, measured with
tools/bench_openai.py --tokens 256 --reps 2 --temperatures 0,tools/bench_concurrent.py --levels 1,4 --tokens 128 --reps 2 --temperatures 0 --alone --serial, andbench/d4_copy.py(four more alternated launches on the same table; 256 tokens, 172 for theparagraph, temperature 0, median of two reps), tok/s:
bench_openai.pyfibonacci-rawbench_openai.pygpu-chat-no-thinkbench_concurrent.pycode, 1 stream (128 tok)bench_concurrent.pychat, 1 stream (128 tok)bench_concurrent.pycode, 4 streams (aggregate / steady)bench_concurrent.pychat, 4 streams (aggregate / steady)d4_copy.pycopy-paragraph, drafted (plain: 46.3 to 47.3)d4_copy.pycopy-8k-list, drafted (plain: 45.0 to 45.9)d4_copy.pychat-control, drafted (plain: 46.8 to 47.3)Four streams at once is unchanged within noise: that path does not use the GPU round's depth rule.
Prompt speed before and after,
tools/bench_cache.py --contexts 2048,8192,32768 --max-tokens 1 --require-coldatserve --parallel 4 --context 34816 --prompt-cache-gib 0on one shared window-cost table (cache_state: "cold"andidentical
token_sha7d7179c146d0,28c7d2ef7f39,ffa6059b954aon every run), median TTFT (and tok/s). Taken ond46d17a (1.1.0 and docs), this branch's previous base; the prompt pass does not read the round depth, and
d1018b3 changes only imports in the Nemotron Metal path:
Peak physical footprint (
vmmap -summary, after the benchmark suites): 23.0 GiB current and 23.0 GiB peak on allfour decode launches of both builds; 22.3 to 22.4 GiB current and 22.7 GiB peak on the prefill launches. 0 leak lines
in every server log.
Tests run, uncached, on this branch and on main at d1018b3:
zig fmt --check build.zig zig,git diff --check origin/main,zig build -Dcpu=apple_m1,zig build test -Dcpu=apple_m1(4930/4951 passed, 21 skipped; main4927/4948, 21 skipped),
zig build test-kernel-contract(65/65),zig build test-golden(306/319 and 26/26 equal, 0unexpected differences, as on main),
zig build fuzz-cortheon(11/11),flashnext_batch_serve_test.py(8 OK),cross-builds
-Dtarget=x86_64-linux-gnu -Dcpu=x86_64_v3and-Dtarget=aarch64-linux-gnu,zig build native -Dcpu=apple_m1 -Dversion=1.0.0, andlean_new.py(443 problems, 444 on the base, 0 new). The 3 more tests are the 2new ones and 1 existing one the macOS test binary now reaches through
round_depth.zig(copy rows that keep landing price a long window; ones that miss price none), all passing. New, inround_depth.zig:a steep per-row window table starts at 1 level and steps up only when windows saturate(expected 1, found 4whenDepth.initstarts atmain's
@min(4, cap), the only failure) anda flat tile window table starts at 4 levels, steps up on full windows, and drops back when deep levels miss(an invented flat table, see below). They run in the macOS test binary; theubuntu and Windows jobs do not build the Nemotron Metal family.
Not run, and why: no M5 or other tensor-unit chip here. On such a chip the rows of a window share one tile, so the
table is flat to 16 rows and this rule will start deeper than 1; main starts at 8 and drops to 4. Whether the start
the table picks matches or beats main there is unmeasured, and the flat table in the test is invented. Please run
tensorfold run --gpu-round 8on an M5, before and after, before this lands. M1 to M3 and CUDA: not run (CUDA isnot touched).
Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.(Above: drafted equals plain 32 of 32 on the CLI;
bench_openai.py,bench_concurrent.py --alone --serial, andd4_copy.pyall match across main and this branch.)every reply's tokens are equal.)
SUMMARYline is in the receipt. (n/a: no kernel is added or changed.)bench_cache.py --require-coldat 2K, 8K and 32K.)the open question above.)