Repository navigation
DeepSeek V4.1: the indexer's merge and finish on a threadgroup a row, the same picks; a decode row's indexer 13-34x faster - #2
Conversation
… the same picks; a decode row's indexer 13-34x faster dsv41_index_merge took each tile's 4,096 scores into a row's 512-pick heap (and its 2,048 candidate blocks) on one thread a row, and dsv41_index_finish sorted them by insertion. Both now run a threadgroup of 1,024 threads a row: - merge: the held picks and the tile's finite scores as 64-bit keys (score as ordered bits, then position reversed, the order dsv41_less gives), the 512th best found by eight 8-bit radix passes, and every pick at or above it kept, placed in thread order (byte-identical runs). The candidate blocks the same way, the newest visible block pinned as before. - finish: a bitonic sort by position, -1 placeholders last, as dsv41_sort_positions orders them. Positions are unique, so the 512 (and 2,048) kept are the heap's own; the finished buffers are byte-equal. The serial kernels stay as dsv41_index_merge_serial and dsv41_index_finish_serial, the baseline for: - index_select_test.zig: the whole pass, parallel against serial, heaps, candidates and positions byte for byte (1, 3 and 17 rows; 37 to 20,000 keys over one to five tiles; random and tied scores; the candidate source pass, then a pass over its candidates). It fails when the merge keeps one pick too few. - The kernel contract (metal_dsv41_select.zig): the merge against a host oracle (its picks as sets), the finish bit-equal to the serial finish; gates 1-4 pass for both, and gate 5 at 1,024 threads (offline M1 and M2: 1,024). Whole indexer pass a layer (score tiles, merges, finish), M5 Ultra, idle GPU, two runs each (1eccf66 -> this): - 1 row: 8,192 keys 52.7-52.8 -> 0.26-0.50 ms; 131,072 keys 129.9-130.8 -> 3.69-3.81 ms; 520,000 keys 212.2-213.8 -> 15.63-15.66 ms; with candidates (16,384 keys) 11.61-11.66 -> 0.34-0.35 ms. - 256 rows: 8,192 keys 97.1-97.3 -> 11.66-11.71 ms; 131,072 keys 540.7-541.1 -> 188.48-188.49 ms; 520,000 keys 1,320.5-1,320.9 -> 750.0-750.4 ms; with candidates 57.81-57.84 -> 23.53-23.57 ms. Not run: the full model. The family keeps the 2.9 bpw EXL3 pack and the Engram tables resident (about 414 GB), more than the 256 GB this Mac has.
akol1
left a comment
There was a problem hiding this comment.
Verdict: approve with nits, all folded in — refreshed for the new family head.
Base note. This PR sits on pr-dsv41-bc-family (1eccf660), which is no longer the family head. The family has been reconciled onto upstream 1.1.1, and in that stack the family's Metal sources moved to zig/src/families/deepseek_v41/metal/. I rebased your commit onto 4f69fea10 and kept it as its own commit (a17207e70, your message, body and authorship, same content). Only relocation: your new test moved to zig/src/families/deepseek_v41/metal/index_select_test.zig and is imported from zig/deepseek_v41_checks.zig in the same place it was before. The SUMMARY 91 kernels line in your body was the old head's full run; I left your body alone.
The branch to take: pr-dsv41-bob-indexer on akol1/TensorFold = your commit a17207e70 plus my review commit 48a64f450 (10 files, +516/−6 against 4f69fea10).
Answered:
- Score and clear registration: not added, and I don't think they belong in this change.
dsv41_index_scoreanddsv41_index_clearare unchanged by this PR and were unregistered before it. Registering a launch is more than a row: it needs alaunchOfmapping inmetal_limits.zig, a spec and an oracle, andregistry.zigis exactly at the 600-line file cap right now. It is the same tracked coverage item that would take the rest of the family's relocated kernels, not a gap this PR opens. - The hard-coded 1,024 is gone.
metal_dsv41_select.zignow dispatchescsa2_index.select_threadsthrough adeepseek_v41_indexexport ontensorfold, the constant the engine launch itself uses, so the test cannot drift from the launch. - The 16 KB array has its comment.
threadgroup Dsv41Pick tmp[2048]is needed at launch, before therow >= p.rowsguard, which is why the gate-5 row stays 16,384 bytes for the last partial rowgroup. One comment; no code change. - The finish path's candidate oracle is now compared, not just computed. It asserts the finish leaves the candidate blocks in
dsv41_sort_positionsorder too (row {d}: the candidate blocks out of position order); before, only the merge compared them against the oracle. - Gate 5 and the suite receipt are below.
What I re-ran (Apple M3 Ultra, 80-core GPU, 512 GB unified memory; macOS 26.6.2, Xcode 27.0; Zig 0.17.0; fresh local build caches; family head + your commit + the review commit):
zig build test-deepseek-v41 test-kernel-contract --summary all: 52/52 steps succeeded, 144/144 tests passed.zig build test --summary all: 360/360 steps succeeded, 5,038/5,059 tests passed (21 skipped).tf-kernel-contract --only deepseek_v41 --no-limits:SUMMARY 2 kernels: 2 pass gates 1-4, 0 fail, 0 known findings. Gate 5 on this GPU: merge 1,024 threads and 1,168 threadgroup bytes, finish 1,024 threads and 16,384 bytes (this GPU's most 32,768).- The limits half of that run, unfiltered: 161 launches, 12 problems — the same 12 this branch's base has (159 launches, 12 problems at
4f69fea10), all flashnext entries with no compiled limit recorded; your two launches are clean. That is why the filtered gate run above passes--no-limits. zig fmt --checkclean;git diff --checkclean;python3 tools/zig/lean_check.py: 3,396 files and 444 problems, and against the base's 3,394 files and 444 problems that is no new finding (both added files are clean).- Not re-measured: the timing table. It stays your M5 Ultra numbers, and I am claiming nothing independent about speed. Not run: the full model — the resident layout fits on this host, but the selections are byte-equal, so the model logits cannot move, and whole-model/residency belongs to the tracker gate (ashhart#847) rather than to a review of this change.
If you would rather keep this PR's head yours: git fetch https://github.com/akol1/TensorFold pr-dsv41-bob-indexer && git reset --hard FETCH_HEAD && git push -f origin dsv41-index-select, and I will retarget this PR at pr-dsv41-bc-family-11 (the current family head) so it shows just the change. Otherwise the 1.1.2 family carry takes pr-dsv41-bob-indexer as it stands, with your commit first and mine second, and your authorship intact.
|
This is superseded: you rebased the pair onto |
Following up on ashhart#847: the indexer change from our engine that keeps the bytes and fits your layout. It's against your
pr-dsv41-bc-family.The other one isn't needed. Your layers after the candidate source already score only their candidate blocks (
use_candidates), which is what our first indexer commit did.Where the time goes. In your indexer pass, most of the time is the merge: one thread per row pushes each tile's 4,096 scores through the 512-pick heap (and the 2,048 candidate blocks). Then
finishsorts them on that thread too. For a decode row that's almost the whole pass. We also tried tilingdsv41_index_score(byte-equal to it): on an idle GPU it was 1-6% slower, so it isn't in this PR.The change: merge and finish on a 1,024-thread group per row.
dsv41_lessgives. Eight 8-bit radix passes find the 512th best key, and every pick at or above it is kept, written in thread order so runs are byte-identical. The candidate blocks are selected the same way, with the newest visible block pinned as before.dsv41_sort_positions' order.dsv41_index_merge_serialanddsv41_index_finish_serial, as the baseline for the checks below.Checks (M5 Ultra; this is the only chip we ran):
index_select_test.zig(new, intest-deepseek-v41): the whole pass, parallel against serial, with heaps, candidates and positions compared byte for byte. Cases: 1, 3 and 17 rows; 37 to 20,000 keys over one to five tiles; random and tied scores; the candidate source pass, then a pass over its candidates. It fails when the merge keeps one pick too few.test-deepseek-v41: 80/80 in ReleaseFast and Debug, including your top-512 tie and candidate-pinning tests.metal_dsv41_select.zig):replaces).docs/kernel-contract.md.SUMMARY 91 kernels: 91 pass gates 1-4, 0 fail, 2 known findings; 14 controls: 14 red as planted, 0 missed; 0 limit problemstest-kernel-contract: 36/36.zig fmtclean; the lean check is unchanged from your head (502).Timings. The whole indexer pass per layer (score tiles, merges, finish), M5 Ultra, idle GPU, two runs each,
1eccf660→ this:Not run: the full model. The family keeps the 2.9 bpw EXL3 pack and the Engram tables resident, about 414 GB, and this Mac has 256 GB. The selections are byte-equal, so the logits can't change, but we haven't measured end-to-end speed. If you run
tf-dsv41-run profileorteacheron a larger Mac, the indexer's share is what this should move.Glad to rework it to fit your next increment.