[Perf] Ship the qualified SM70 long-context layout and drop its context fences - #609
Conversation
…xt fences The grouped E4M3 FP32 long-context operator entered the tree in #602 under the source digest of the qualified candidate (eb7a85511f581fcd22cf13619c85ed2f42a8cbc8b216bb3e632bf448f6b820e1, "P layout, early store, E4M3 lookup") but with the body of an earlier revision of that candidate, heads1-qk2-vector16-page-prefetch (8459d57c6b72). All five header files are byte-identical between the two, so neither the build, nor the manifest check, nor the tests could tell them apart; only the kernel body differs, by 669 lines. The earlier revision lacks the probability swizzle, the early probability store, PV-value reuse, the full-q8 specialization, the all-visible tile walk and the shared E4M3 lookup table that the recorded digest names. csrc/attention/sm70_grouped_long/kernel/grouped-attention.cu now carries the digest the manifest already claimed, and BUILTIN_MANIFEST records that digest so a rebuilt kernel cannot inherit the workspace identity of a different layout. Its only difference from the qualified candidate is registration: PYBIND11_MODULE becomes TORCH_LIBRARY_FRAGMENT plus a double-signature adapter, and torch/extension.h is replaced by torch/library.h with an explicit c10/cuda/CUDAException.h. The other five files are unchanged. Also in this change: * MAX_CONTEXT returns to 262144. #602 raised the bound to 262152 for one page of generation headroom, but 262152 is larger than the 262144 capacity the route exists to cover, and it is not what any descriptor bucket can equal. * The four places that compared a context bound against a literal 262144 now compare against the live long-attention contract. Under 262152 those comparisons silently dropped the q1 verifier tail out of the long-context graph variant, which is why #602 measured 38.24 ms at 261888 with the whole tail on the eager path. * The compact scalar tail operator ships inside _vllm_fa2_C and is selected with no environment variable; VLLM_SM70_DFLASH2_SCALAR_ATTENTION_MANIFEST stays as an override for an externally built candidate. Admission compares the graph descriptor bucket against the declared contract and requires the manifest to cover it, instead of testing for exactly 262144. * VLLM_SM70_DFLASH2_TAIL_CUDAGRAPHS defaults on. The eager tail was the dominant round cost at the capacity and 1K/128K are unchanged; =0 restores it. * The 256K contract admits the whole B1 verifier tail range again (query_rows [2..8]). An earlier revision of this change narrowed it to [8] after measuring a regression, but that regression belonged to the predecessor kernel: with the qualified layout the tail widths do route through the long-context operator, and the qualified candidate was measured with [2..8]. Measured on four V100-SXM2-32GB, TP4/B1, E4M3 target KV, the PR596 serving environment, the official request path and the PR596 same-startup control/candidate protocol (alternating arms inside one serving session, three steady pairs per cell, median of request averages): context/seed control candidate rounds accepted/round 1024/0 17.439 17.460 54 3.778 131072/0 23.802 23.780 59 3.339 261888/0 36.654 32.834 50 4.020 261888/2 38.451 32.669 44 4.727 Both arms produced byte-identical token ids, finish reasons, round counts, accepted-draft counts and emitted-tokens-per-round. The same cells measured with the qualified external DSOs were 15.81-15.90, 22.56-22.89, 35.24-36.20 -> 31.39-31.47 and 37.32-37.88 -> 31.25-31.35. Every cell of this run sits 1.5-10% above those, including the 1024 cell, which touches no long-context route at all, so the residual is host contention and not the operator; 1024 and 131072 differ between arms by less than 0.03 ms in both runs. At 261888 the shipped default therefore moves #602's 38.24 ms to 32.8 ms, with no regression at 1K or 128K. Assisted-by: DeepSeek Harness Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com> The two ported kernels are listed in the clang-format exclude alongside the existing verbatim ports, so the shipped body stays byte-comparable with the digest its manifest records.
…t a literal The route captured its graph and admitted its operators at a hard-coded 262144. That is the capacity the shipped kernel was qualified for, but it is not the context a deployment necessarily serves, and every consumer compared against the literal: the captured graph bound, the compact scalar tail's descriptor bucket and max_seq_len_hint, and the q1 tail threshold at 131072. A model served with a different --max-model-len therefore ran a route pinned to someone else's window, and any change to the constant silently turned pieces of the route off -- which is how the q1 verifier tail ended up on the eager path in #602. The contract now separates the two ideas: * long_attention_capability(manifest) is the context the operator was qualified for. It stays a manifest field and it is the ceiling. * long_attention_contract(manifest, capacity) returns the bound the graph is captured at: min(capability, capacity), where capacity is the model's configured max_model_len. The served window decides the route, and the operator is never driven past what it was admitted for. * The graph builder stamps that bound onto the descriptor and the wrappers admit any bucket within their capability, instead of re-deriving the window. The two can no longer drift apart and only one place reads the service capacity. * The q1 tail threshold becomes half the captured bound rather than 131072, so a different served window keeps the same behaviour. * The compact scalar tail is reported inactive, rather than misconfigured, when the long-context route is off. Verified on the reference serving environment with three served windows. The captured bound follows the flag while the operator capability stays 262144, and 1K is unaffected by the bound: --max-model-len captured graph bound capability 1K steady (ms) 262144 262144 262144 16.003 15.961 15.922 131072 131072 262144 16.178 15.963 15.901 65536 65536 262144 15.876 16.122 15.989 The long route is not what the 1K round pays for: with VLLM_SM70_E4M3_LONG_ATTENTION=0 the same request costs 16.370/16.378/16.324 ms in an adjacent window. The tail graph tests are updated for the new contract, and three expectations they still carried from the 132096 era -- a default of 132096, a rejected max_context above 262144, and the manifest variable as the on/off switch -- are corrected. tests/v1/worker/test_sm70_long_attention_graphs.py, tests/v1/attention/test_sm70_e4m3_scalar_tail.py and tests/v1/attention/test_sm70_e4m3_grouped.py go from 5 failures and 44 passes to 51 passes. Assisted-by: DeepSeek Harness Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Follow-up commit: the context bound is no longer a literal
The contract now separates capability from the served window:
Verified across three served windows
The bound follows the flag; the operator capability does not move. 1K is unaffected by the bound. On the 1K numberThe 1K round is 15.88–16.12 ms here, which matches the PR596 reference (15.81–15.90). The 16.8–19.2 ms figures seen earlier in this work came from host contention — a co-tenant was running on the other four cards — not from the route. As a control, the same request with Tests
|
What
#602moved the grouped E4M3 FP32 long-context operator into the tree under the source digest of the qualified candidate but shipped the body of an earlier revision of that candidate.#602eb7a85511f58…f6b820e1(4,985 lines)8459d57c6b72…f070f738(4,316 lines)sm70_grouped_swizzle_eb7a85511f58sm70_grouped_attention_8459d57c6b729db33737adb8…62fee21dac8262f3d02…76a97339All five header files are byte-identical between the two revisions, so neither the build, nor the manifest check, nor any test could tell them apart. Only the kernel body differs, by 669 lines: the shipped revision lacks the probability swizzle, the early probability store, PV-value reuse, the full-q8 specialization, the all-visible tile walk and the shared E4M3 lookup table that the recorded digest names.
This PR ships the layout the manifest already claimed, and removes the literal context fences that also kept the q1 verifier tail off its graph.
Changes
grouped-attention.cureplaced with the qualified source. Its only difference from the candidate is registration (PYBIND11_MODULE→TORCH_LIBRARY_FRAGMENT+ adouble-signature adapter;torch/extension.h→torch/library.h+ explicitc10/cuda/CUDAException.h).BUILTIN_MANIFESTnow records that source digest so a rebuilt kernel cannot inherit the workspace identity of a different layout.MAX_CONTEXTreturns to262144.#602raised it to262152for one page of headroom, but no descriptor bucket can equal262152, and it is larger than the capacity the route exists to cover.262144now compare against the live long-attention contract. Under262152they silently dropped the q1 tail out of the long-context graph variant — that is why#602measured 38.24 ms at 261888 with the whole tail eager._vllm_fa2_Cand needs no environment variable.VLLM_SM70_DFLASH2_SCALAR_ATTENTION_MANIFESTremains an override for an external candidate.VLLM_SM70_DFLASH2_TAIL_CUDAGRAPHSdefaults on;=0restores the eager tail.query_rows [2..8]). An earlier revision of this work narrowed it to[8]after measuring a regression, but that regression belonged to the predecessor kernel.clang-formatexclude, so the shipped body stays byte-comparable with the digest its manifest records.Measurement
Four V100-SXM2-32GB, TP4/B1, E4M3 target KV, the PR596 serving environment, the official request path, and the PR596 same-startup protocol (alternating
tail_switcharms inside one serving session, three steady pairs per cell, median of request averages):Both arms produced byte-identical token ids, finish reasons, round counts, accepted-draft counts and emitted-tokens-per-round.
The same cells measured with the qualified external DSOs were 15.81–15.90, 22.56–22.89, 35.24–36.20 → 31.39–31.47 and 37.32–37.88 → 31.25–31.35. Every cell of this run sits 1.5–10% above those — including the 1024 cell, which touches no long-context route at all — so the residual is host contention (a co-tenant was running on the other four cards), not the operator. 1024 and 131072 differ between arms by less than 0.03 ms in both runs.
Net effect at the capacity:
#602's 38.24 ms → 32.8 ms, no regression at 1K or 128K.Caveats
261888/seed0and261888/seed2cells show any arm difference; 1024 and 131072 are controls.Assisted-by: DeepSeek Harness