Repository navigation
[Engine] score serial token logprobs on GPU - #2
xiezhipeng-git merged 2 commits into
Conversation
|
I built and ran both branches end-to-end (real model, real logprobs requests) before approving the workflow run. Setup
Disclosure: the builds, the server runs and the log/result parsing here were executed with AI assistance (an assistant driving the build + benchmark scripts). Every number below comes from an actual run on this machine — nothing is estimated — but treat the write-up as machine-assisted analysis, and the code review as mine. 1. Build and test pass rates
The extra PR test is 2. Speed — this is where the PR pays off Decode throughput, 200 tokens, engine's own log line (
So the honest framing is: requesting logprobs used to cost a fifth of decode throughput, and with this PR it is essentially free (−3% off the ceiling). That is a bigger deal than the microbenchmark alone suggests, and it is well outside run-to-run spread (the two runs of each configuration differ by ≤6 tok/s). Kernel-level numbers (Kaggle T4×2):
The old path copies the whole 248,320-way logit row back to the CPU and reduces it there every step; the new path reduces on the GPU and returns only chosen + top-K (672 B = 4 accepted tokens × 168 B). That removed per-step cost is exactly the 36 tok/s the control run shows. 3. Logprob values I ran each branch twice to get a noise floor before attributing any value delta to this PR:
Two things follow. First, base-vs-PR (0.123 / 2.33 / 171) sits at the same-engine noise floor (0.124 / 2.09 / 180), so the differences are the engine's own run-to-run rounding — expert-cache hit rate and the prefill CPU share decide which experts are computed where, and the engine itself logs that the GPU rounds differently from the CPU. Second, that noise is large enough to flip a near-tie: the two PR runs both had ~-0.98 on the chosen token at position 55 and picked different tokens ( Two non-blocking observations
|
|
One optional, non-blocking follow-up from reading the engine side of this PR. Consider allocating the three logprobs buffers in
The reason to consider moving the allocations is failure timing. A first-use The tradeoff is that initialization would reserve this small amount of memory for every applicable verifier even when logprobs are never requested, and an allocation failure would move from first use to startup. Given the tiny fixed size, that fail-fast tradeoff seems reasonable to me, although keeping the lazy allocation is also defensible. If you do move it, please keep Disclosure: I went through the engine side with AI assistance (an assistant reading and searching the sources). The control flow and buffer sizing above were checked against |
|
One note on the boundary this PR draws, which I think is the right one. Keeping named That tradeoff reverses once the named set becomes small and per-request. If upstream Niko1221#3743 lands (typed decisions scoring a few dozen single-token numeric labels per request), the per-token cost becomes the whole cost again, and this is worth revisiting as a separate change. Disclosure: the numbers above come from runs I made on my own hardware and branch; the engine-side reading was done with AI assistance. |
|
Implemented the optional startup-allocation follow-up in 7265fc75. The three buffers now allocate in On this SHA, the full CUDA/SM89 build, kernel parity, real-CUDA initializer/failure/reuse checks, all 33 server cases, and five real-model API cases passed. The updated PR body records the exact coverage, corrected cache configuration, and recovered failed attempt. No throughput or general bit-exact claim is made. HIP compilation/hardware validation remains unavailable, so this PR stays draft. A targeted HIP check would help complete backend validation. |
Serial top-K logprobs currently copy the full vocabulary to the CPU and scan it for each emitted token. This change computes the chosen-token score and top-K on the GPU for ordinary serial decode, including accepted MTP rows, and transfers only those results. For vocabulary 248,320 and K=20, the result download changes from 993,280 to 168 bytes per row. This is a data-size comparison, not a measured throughput result.
This targets the logprobs branch behind Niko1221/Strata#3200. It preserves the
T/LPprotocol and existing server behavior. Namedallowed_token_ids, pipeline windows, gfx906 and unsupported dimensions retain CPU scoring. The reduction uses shifted log-sum-exp, deterministic token-ID ordering for ties, and at most 20 alternatives. GPU rounding and tie ordering can differ from the CPU path. Requests without logprobs launch no new scoring work. No RAM-standby changes are included.The optional startup-allocation follow-up is included:
Verifier::init()allocates the three separate logprobs buffers, so their allocation failures are reported at startup rather than during the first GPU-scored request. Existing!ptrguards preserve those buffers on repeated initialization. Zero scratch size allocates none of them and retains the decoder's CPU fallback; named IDs remain on the existing CPU path. This reserves a small fixed amount even when logprobs are never requested: 83,360 bytes for max_t=8, vocabulary 248,320 and K=20.Validation on
7265fc75dfbbf714cc03d740e95b41f9ea0586fb: Linux, RTX 4090, CUDA 12.8, SM89, GCC 14.3.1. Commands below normalize the isolated build location to$BUILD;$PYTHONis an isolated CPython 3.13.9 environment with repository-pinned dependencies.Passed:
cmake --build "$BUILD" --target strata logprobs_parity -j2. Fresh pinned ggml dependency3cf03257f219afbe7334045ff7c6a06ac68c627d; changed translation units compiled and linked. Build took 7m52s, with 1.5 GiB reported peak host memory and zero swap."$BUILD/logprobs_parity": six numeric cases, including vocabularies 1/4097/8193/248320, K=0/1/20 and K=vocabulary, multiple rows, ties, a chosen token outside top-K, and finite extreme/equal logits. GPU scores match the double-precision CPU reference within 3e-5.Verifier::init()with synthetic metadata and real CUDA allocations. Supported and unsupported-vocabulary cases each initialized twice; the three optional buffers were reused or absent as appropriate. It checked all three injected allocation-error returns, immediate failure, actual destructor cleanup, andVerifier::logprobs()versus CPU reference for rows 1/8 and K=0/20. This verifies the three logprobs buffers, not general repeated-init leak freedom or natural hardware OOM.serve.test_logprobscases passed across compatible runs: 15 non-API cases in$PYTHON -m unittest -v serve.test_logprobs, then all 18 API cases in$PYTHON -m unittest -v serve.test_logprobs.Apiafter resolving the missing dependency described below.Paris.; this is one observed sequence match, not a general bit-exact guarantee. K0/K20 scores can vary across requests.git diff --checkand final tracked-tree checks.Builds and standalone tests used bounded transient systemd units with a 4 GiB host-memory cap and no swap. The isolated full-model unit used an 88 GiB cap and no swap; observed host peak was 61.9 GiB. Its corrected
--expert-cache 6452sets a nominal pool budget below 16 GiB for this pack's largest aligned blob of 2,662,400 bytes; profile/free-room limits produced a 13.27 GiB cache. VRAM reserve was 2048 MiB. Sampled candidate GPU peak was 21,900 MiB, with at least 1,479 MiB physically free. These are correctness-test observations, not benchmark results. The original serving deployment was restored and its text/logprobs health check passed afterward.Failed attempts, recovered:
jinja2; the first isolated full-suite attempt then had 18 API import errors becauseregexwas missing. Repository-pinned dependencies were installed only in the isolated test environment; the 18 affected cases were rerun and passed.--expert-cache 8192was mistakenly treated as a MiB value; the parameter is a count-based byte budget and produced a 14.27 GiB cache. The corrected-count/reserve retry passed all five API cases within the original total resource limits. Failed-run evidence was retained.Not run: HIP compilation/hardware execution; full SYCL/Metal builds or execution; a controlled before/after performance benchmark; long-prefix/long-generation stress. The 128K context value above is a configured limit, not a 128K-prompt test. SYCL/Metal scoring paths are unchanged. Keeping this PR draft under the repository's backend-validation rules; targeted HIP validation is still needed. No throughput improvement is claimed from these tests.
Developed with AI assistance; the diff, actual command results and limits were reviewed before publication.