Repository navigation
fix(Engine): draft head "too many rows" on large draft vocab - #28
Conversation
…ze works The 2-bit draft head's rescore kernel swept its candidates over a fixed RS_SLOTS (12) x 512 grid = 6144 candidates = 49,152 rows, and threw "draft_proxy_top1: too many rows" above that. The MTP drafter enabled the copy on head type and width alone, so a model whose draft_vocab.bin held more than 49,152 tokens (upstream's 106,299 CJK subset) threw on every draft step and the server retried each token. Make the sweep grid-stride: start slot blockIdx.x*RS_W + warp, step gridDim.x*RS_W. That is the exact slot sequence the fixed sweep used, so the rows that once fit rescore in the same order and the sum is bitwise identical; the loop only keeps going past the old cap. cand[] is small and L2-resident, so dropping the register preload costs nothing (the row reads dominate, and the window test skips all but the rows near the best). draft_proxy_fits() now only rejects a count the kernel's int candidate index cannot represent; bind_head keeps it as the fallback gate and STRATA_DRAFT_PROXY=0 stays as the manual off switch. draft_proxy_parity runs the shipped 40,525 subset (control, bit-identical), 49,184 (one tile past the old cap, the shape that used to throw), and the 106,299 CJK subset, and times the copy against the exact head at each size.
…ild) The parity selftest can't run in `docker build` (no GPU) and the runtime image is built STRATA_BUILD_TESTS=OFF, so it carries no test binary. Add a dev image (CUDA toolchain + venv) and a script that runs the selftest against a GPU: docker/Dockerfile.selftest + selftest-draft-proxy.sh. A cached dev image and a gitignored build-selftest/ cache make repeat runs incremental. Usage: sudo ./selftest-draft-proxy.sh (CUDA_ARCH=89-real for one card).
…_TESTS=ON configures The fork's tests/ is Python-only (no CMakeLists.txt), so enabling tests broke configure at add_subdirectory(tests). The C++ parity targets (incl. draft_proxy_parity) are defined in this file, not tests/, so they build regardless. Matches the EXISTS guard the file already uses for other optional subdirs.
A partial CMakeCache.txt from a failed configure made the script skip reconfiguring and jump to a build that then failed. Re-running cmake configure is cheap and recovers, so drop the skip-if-cached guard.
…icro-targets This fork does not ship bench/micro/graph_registry_test.cu or pinned_capture.cu, so STRATA_BUILD_TESTS=ON failed to generate with 'Cannot find source file'. Guard each with EXISTS, matching the neighboring optional micro-targets (native_mmvq_multi, hit_cpu_order_parity).
There was a problem hiding this comment.
🟡 Changes recommended
The eligibility gate accepts row counts that exceed the scoring kernel’s signed integer range.
1 open issue
What changed in this PR
This PR removes the draft proxy’s fixed 49,152-row rescoring cap so the MTP drafter can handle larger draft vocabularies, including the 106,299-token CJK subset.
Changes:
- Replaces the fixed rescoring sweep with grid-stride iteration and adds an eligibility gate.
- Expands parity checks and timing measurements across three vocabulary sizes for Q6_K and Q8_0.
- Adds a Docker-based selftest runner and guards optional CMake test sources.
| File | Description |
|---|---|
| src/kernels/draft_proxy_parity.cpp | Tests and times three vocabulary sizes. |
| src/kernels/cuda/draft_proxy.cu | Adds grid-stride rescoring and row-count eligibility checking. |
| src/core/mtp.cpp | Gates proxy use and logs exact-head fallback. |
| selftest-draft-proxy.sh | Builds and runs the GPU selftest in Docker. |
| include/strata/kernels/draft_proxy.hpp | Declares and documents the eligibility API. |
| docker/Dockerfile.selftest | Provides the selftest development environment. |
| CMakeLists.txt | Guards optional test sources and the C++ test directory. |
Review effort: Balanced
| // The rescore sweep is grid-stride, so there is no fixed row cap: any vocabulary that fits in memory works. The | ||
| // only real limit is the rescoring kernel's int candidate count (n_tiles * PER); kept as the single place a future | ||
| // fixed-size optimization would have to respect, and the reason the caller falls back to the exact head. | ||
| if (n_rows <= 0) return false; | ||
| return tiles_of(n_rows) * PER <= (int64_t) INT32_MAX; |
…w count past the kernels' int itself; the comments describe the current sweep The gate let counts past INT32_MAX through to the kernels, which take the rows and their ids as ints (GitHub's review), and no draft head comes near it: 24 GiB holds 9-12 M rows of Q8_0's 2,720 or Q6_K's 2,100 bytes. draft_proxy_top1 refuses n_rows outside 1..INT32_MAX; the drafter takes the copy for every head type and width it serves and names the others at the start, as #28 has it. The sweep reads each candidate from L2 as it comes (#28). draft_proxy_parity's timing on the 3090, medians of 6 runs alternating: at 40,525 rows Q6_K 50.9 us against 52.4 with the 12 candidates a warp loaded ahead (custom's kernel), Q8_0 50.2 against 52.0; #28's sweep with 12 loaded ahead a pass, the same slot order, took 1.0-2.1 us longer at 40,525, 49,184 and 106,299 rows. The test passes at all three sizes on the 3090 and the 5070 Ti. Window hashes identical to custom's before #28 (think, 1,200 tokens, --adapt-every 0: IQ3_S 499 windows, UD-Q4_K_XL 586); drafting a round 1.070 -> 1.064 ms (IQ3_S), 0.938 -> 0.936 (UD). CMakeLists.txt: the guards' comments (tests/ holds C++ tests as well; what the published source leaves out is the Catch2 suite's tests/CMakeLists.txt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, merged. This branch's own draft subset is 40,525 tokens (its What I checked on this machine (RTX 3090 + RTX 5070 Ti, Windows):
One follow-up (a85301c):
|

Problem and result
The MTP drafter's 2-bit draft-head copy (its fast top-1 argmax) capped at 49,152 rows and threw
draft_proxy_top1: too many rowsabove that, but the drafter enabled the copy on head type and width alone - it never checked the row count. Any model whosedraft_vocab.binis larger than 49,152 tokens - upstream's default 106,299-token CJK subset - threw on every draft step, so the server retried each token.After: the copy scores any draft vocab. The parity selftest passes at 40,525 / 49,184 / 106,299 rows for Q6_K and Q8_0, and the draft-head argmax at 106,299 rows is ~2.4-3.1x faster than the exact head.
Related discussion
none. This is fork-only: the 2-bit draft proxy is not in upstream, so the bug does not exist there (upstream always uses the exact head, which has no row cap).
Approach
The rescore kernel swept its candidates over a fixed
RS_SLOTS(12) x 512grid = 6,144 candidates = 49,152 rows. The sweep is now grid-stride:start = blockIdx.x*RS_W + warp,step = gridDim.x*RS_W. That is the exact slot sequence the fixed sweep used, so the rows that once fit rescore in the same order and the sum is bitwise identical; the loop only keeps going past the old cap.cand[]is small and L2-resident, so dropping the register preload costs nothing (the row reads dominate, and the window test skips all but the rows near the best regardless of size).draft_proxy_fits()now only rejects a count the kernel's int candidate index cannot represent;bind_headkeeps it as the fallback gate, andSTRATA_DRAFT_PROXY=0stays as the manual off switch.Supporting changes so the selftest can actually run:
STRATA_BUILD_TESTS=ONfailed to configure in this fork becausetests/is Python-only and twobench/microsources are absent - both are now guarded withEXISTS, matching the pattern the file already uses for other optional sources. Added a one-command selftest (selftest-draft-proxy.sh+docker/Dockerfile.selftest), sincedocker buildhas no GPU and the runtime image is builtSTRATA_BUILD_TESTS=OFF.Validation
Passed:
sudo ./selftest-draft-proxy.sh@ 12089e3, CUDA 13.0, RTX 4090. All sizesok; probability within 0.02-0.03 of exact; 0 near-ties picked otherwise.Failed:
Not run:
tools/strata_bench.pyA/B (same image,STRATA_DRAFT_PROXY0 vs 1, 106k CJK subset) showed no significant difference: the suite's noise floor (~2%, measured via prefill, which the proxy cannot affect) exceeds the expected ~1-2% decode effect, and decode had ±20% run-to-run variance at n=3. The parity timing above is the authoritative measurement of the draft-head cost.Limits