Repository navigation
[Engine] Maxwell: the dense projections' FP16 GEMMs as cublasSgemm on FP32 copies (1.18-1.65x, bitwise) - #3596
Closed
msprengholz wants to merge 4 commits into
Closed
msprengholz wants to merge 4 commits into
msprengholz wants to merge 4 commits into
Conversation
…ards CUDA 13 dropped Maxwell, so those cards need the experimental CUDA 12 engine, and that engine refused anything below compute capability 6.0. A GTX TITAN X (sm_52) is a 12 GB card with enough memory for a small model, and the kernels compile for it, so the gate was the only thing in the way. The gate now admits 5.0, setup's card choice admits the Maxwell cards (and still refuses Kepler, sm_35, for which no engine code exists), and the engine's own check says 5.0 or newer in the experimental build. The release CUDA 13 engine is unchanged: it still needs 7.5. Measured on a GeForce GTX TITAN X (sm_52, 12 GB, driver 580.178.04, CUDA 12.9): the engine loads, serves and answers, and the Gyro-S profile in bench/results/2026-10-21-gyro-titanx is that card. Validation: tools/test_setup_choices.py (50 tests), tools/test_setup_older_gpus.py (19) and tools/test_setup_configs.py pass; cmake --build build-cuda12 with -DCMAKE_CUDA_ARCHITECTURES=50 -DSTRATA_EXPERIMENTAL_SM60=ON is clean. The HIP, SYCL and Metal engines are untouched by this change and were not built here (no ROCm, no Intel toolchain on this PC).
…ies (1.18-1.65x) On a GeForce GTX TITAN X (sm_52) the dense projections' FP16-operand GEMMs go through cuBLAS's Maxwell kernels, maxwell_sgemm_fp16_32x128 and _64x64, and they are the second biggest compute item of a prompt: 1.93 s of the 9.8 s the GPU spends on a 1,024-token prompt read. The same route as on the P40 - cublasSgemm on FP32 copies of the FP16 operands - is faster here, because this card has no tensor-core path for the FP16 call at all. The route was gated to compute capability 6.x; it now also accepts 5.x, the cards whose cuBLAS has no tensor-core path for this call. It stays opt-in (STRATA_DENSE_F32=1) and the default path is untouched. Measured on the TITAN X (sm_52, 12 GB, driver 580.178.04, CUDA 12.9, the experimental CUDA 12 build, engine 0.1.46.1, Gyro-S with the repo's strata-gyro-s.json): gemm_f32_parity (CUDA 12.9's cuBLAS): 5,201 chunk sizes bitwise; the route is 1.18-1.65x for the big shapes (qkv 10240x2560 at T 7,040: 166.5 -> 120.0 ms; query 12288x2560 at T 7,041: 203.4 -> 149.8; gate 6144x2560: 101.0 -> 67.8; out 2560x6144: 127.5 -> 77.1) and neutral from T 560 to 2,048. Through the server, the prompt read: 91.5 -> 95.7 tok/s (the medians of three repeats, the page cache evicted before each run; the two base runs agree to 0.2 % on that number). The answers are the same, character for character. The profile that found it is in the same commit: an Nsight Systems trace of a real run (every kernel, its time, its count, the copies, the idle gaps), the tool that turns a window of the trace into the tables, the A/B harness that measures a candidate (a warmup request, an evicted page cache, the engine's own per-window phase split), and the ranked candidate list with every candidate's number - including the eight that measured slower and were dropped. The profile's own answer, for the record: on this card the GPU computes 9.0 s of a 53 s decode and waits 40.8 s for the expert data to come off the SSD (the experts are 18 GiB, the card has 12 GiB of VRAM and the PC 15.7 GiB of RAM). The kernels are worth working on where the data fits; here the data is the limit. The write-up says so, with the numbers, in docs/OLDER_GPUS.md and docs/GYRO.md.
…cture The engine has an interleaved small-batch matvec (a warp takes 2 or 4 rows, so the weights and the activations are reused across rows) with a table per architecture - kIlRows_60 for a P100, kIlRows_61 for a P40, kIlRows_70 for a V100 - but il_arch_ok stopped at compute capability 6, so a Maxwell card never used it. That is where the gain is on a card without dp4a: the kernels are integer-bound, so the reuse pays. The table is read off mmvq_il_parity --bench --emit-table on a GeForce GTX TITAN X (sm_52, 12 GB, 24 SMs, CUDA 12.9), the repo's documented procedure, and the parity tool says every case, T 1-8, rows 1/2/4 and the table, bitwise. The switch is opt-in (STRATA_SM50_TABLE=1): the table is one card's measurement. Measured on the TITAN X, the engine's own stage timers (STRATA_VERIFY_PROFILE=1), the medians of 3 repeats, the page cache evicted before each run: the dense projections' stage 8.92 -> 7.76 ms per window (-13 %, 3 of 3) the VRAM expert hits 36.7 -> 34.5 ms per window ( -6 %) The request's tok/s does not move, and the report says why: on this card the GPU is idle 80 % of the time - it waits 190-310 ms per window for the experts while it computes about 40 ms. The change is for the cards where the experts fit and the compute is the limit. The answers are the same (parity-answers-sm50-table.json: the same prompt with and without the switch, greedy, character for character). The report and the docs grow with it: every hot kernel now named with its source file and its job, the four things this card does not have measured on it (no __dp4a and no vdp4a: the four-vmad fallback is the best exact form here, 2.37x over the byte-wise one; no cp.async: the binary has zero CP.ASYNC, so the staged hyper-connection read is the "split" variant only, although the start-up message claims otherwise; no tensor cores: the dense projections' FP16 GEMMs are slow, which the FP32 route in the previous commit fixes; 48 KB of shared memory per block: the GDN's staged down projection takes 40 KB of it, so an sm_50 SM holds two of them where the registers would allow three), and the ranked "what to do next" list in the order the profile gives it.
The section said which kernel family the 48 KB shared-memory limit bites on without naming it; the report's table has the names and the sources.
Author
|
Closing this one: the kernel optimizations (the FP32 dense-GEMM route for cc 5.x and the interleaved-matvec table for sm_52) were not asked for. The Maxwell support itself stays in #3595, which is the only part that is needed for the engine to run on a sm_50 card. |
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.
The problem
Two things cost time on a Maxwell card (compute capability 5.x), and both are measured here on a GTX TITAN X:
(
maxwell_sgemm_fp16_32x128,_64x64), and there is no tensor-core path for that call on such a card. They arethe second biggest compute item of a prompt read: 1.93 s of the 9.8 s the GPU spends reading a 1,024-token
prompt. As on the P40 (compute capability 6.1), the same work as
cublasSgemmon FP32 copies of the FP16operands is faster - 1.18-1.65x for the big shapes on this card - and bit for bit the same.
weights and the activations are reused across rows) with a table per architecture - a P100, a P40, a V100 - but
il_arch_okstopped at compute capability 6, so a Maxwell card never used it. That is where the gain is on acard without
__dp4a: the kernels are integer-bound, so the reuse pays.What this changes
src/prefill/gemm.cu: the route (STRATA_DENSE_F32=1, opt-in, unchanged as a switch) was gated to computecapability 6.x; it now also accepts 5.x, the cards whose cuBLAS has no tensor-core path for this call.
src/kernels/cuda/native_mmvq.cu: the interleaved matvec's table for this architecture, read offmmvq_il_parity --bench --emit-tableon the card (the repo's documented procedure), with its own opt-in switch(
STRATA_SM50_TABLE=1).run (every kernel with its time and count, the copies, the idle gaps), the tool that turns a window of the trace
into the tables, the A/B harness that measures a candidate, and the ranked candidate list with every candidate's
number - the nine that measured slower and were dropped included.
docs/OLDER_GPUS.mdanddocs/GYRO.md: what the profile says, with the numbers, the limits of the card, what wasnot measured, and the ranked next steps in the order the profile gives them.
The first commit of this branch is the previous pull request (the Maxwell support the experimental CUDA 12 build
needs); the changes themselves are the two commits after it.
The profile, in one table (the TITAN X, a real 1,024-token prompt and 201 decoded tokens)
The decode's 40.8 s of "busy" is one thread spinning on the doorbell (
wait_flag_ge_kernel) while the host waits forthe expert data off the SSD: the experts are 18 GiB, the card has 12 GiB of VRAM and the PC 15.7 GiB of RAM. The
kernels are 9.0 s: the trellis expert kernels 3.30 s, the dense weights' MMVQ 1.72 s, the GDN hyper-connection
1.01 s, the head and the embedding 0.43 s, the attention 0.11 s, the MTP draft head 0.02 s. Every one of them is
named with its source file and its job in the report, and the four things this card does not have are measured on it
(no
__dp4aand novdp4a; nocp.async- the binary has zeroCP.ASYNC, so the staged hyper-connection read isthe "split" variant only, although the start-up message claims otherwise; no tensor cores; 48 KB of shared memory per
block, which the GDN's staged down projection uses 40 KB of, so an sm_50 SM holds two of them where the registers
would allow three).
Validation
Environment: GeForce GTX TITAN X (sm_52, 12,288 MiB, 24 SMs), driver 580.178.04, Linux 6.2 (CachyOS), CUDA 12.9 at
/opt/cuda, host compiler g++-14. Engine: the experimental CUDA 12 build
(
-DCMAKE_CUDA_ARCHITECTURES=50 -DSTRATA_EXPERIMENTAL_SM60=ON), engine 0.1.46.1. Model: Gyro-S (TQ1_0) with therepo's
strata-gyro-s.json(MTP on,--spec 4,--expert-cache auto,--resident-budget-gib 6,--kv int8).Passed
gemm_f32_parity --quickand--bench(the repo's own tool, built with this CUDA 12.9): 5,201 chunk sizes bitwiseequal, and the route 1.18-1.65x faster for the big shapes (qkv 10,240x2,560 at T 7,040: 166.5 -> 120.0 ms; query
12,288x2,560 at T 7,041: 203.4 -> 149.8; gate 6,144x2,560: 101.0 -> 67.8; out 2,560x6,144: 127.5 -> 77.1), neutral
from T 560 to 2,048 (0.97-1.06x).
mmvq_il_parity --bench --emit-table(the repo's own tool): every case, T 1-8, rows 1/2/4 and the table, bitwise.STRATA_DENSE_F32=1(the medians of three repeats, thepage cache evicted before each run; the two base runs of the same build agree to 0.2 % on that number).
STRATA_SM50_TABLE=1, the engine's own stage timers (STRATA_VERIFY_PROFILE=1): the dense projections'stage 8.92 -> 7.76 ms per window (-13 %, better in 3 of 3 repeats), the VRAM expert hits 36.7 -> 34.5 ms (-6 %). The
request's tok/s does not move, because the GPU is idle 80 % of the time on this card (it waits 190-310 ms per
window for the experts while it computes about 40 ms) - the change is for the cards where the experts fit.
parity-check.py, the same prompt through theserver with and without the switch, greedy;
parity-answers-dense_f32.json,parity-answers-sm50-table.json).cmake --build build-cuda12 -j 8- clean.Failed
ctest:ple_parityandplatform_memory_testfail in this environment, and they fail the same way without thesechanges:
ple_paritywants a Q2_0 model file that is not on this PC, andplatform_memory_testwants to lock morememory than this shell's
ulimit -l(8 MB) allows. The other 66 tests pass.Not run
src/prefill/gemm.cuare inside#if defined(STRATA_EXPERIMENTAL_SM60)(lines 738-794), and that macro is defined only in a CUDA build(
CMakeLists.txt:216-219); the SYCL build (sycl/CMakeLists.txt:265) compiles the same file without it. Theinterleaved matvec's table in
src/kernels/cuda/native_mmvq.cuis CUDA-only source, but that file is in thekernel library the HIP build compiles too, and this PC has no ROCm and no Intel toolchain, so I could not build
them to show it. This pull request is a draft for that reason: if you can build the HIP or the SYCL engine on a
machine that has the toolchain, that is the check I am missing.
gemm.cuare not compiledthere (the macro is off by default), and the table's switch is off by default too.
The limits, said plainly
where the model fits in the card's memory; here the data path is the limit, and the profile says so.
gemm_f32_paritywith your toolkit's library first. The matvec table is one card's measurement, so it is opt-intoo.