[Perf] Ship the SM70 grouped long-context route on by default - #602
Merged
Merged
Conversation
The compensated E4M3 FP32 long-context attention existed only as a private
DSO selected by VLLM_SM70_E4M3_LONG_ATTENTION_MANIFEST, and the graph bound
stopped at 132096 tokens, so the route had to be hand-enabled and never
covered the 262144 service capacity. Both are now part of the tree and on by
default.
The operator source moves from the private candidate into
csrc/attention/sm70_grouped_long/ (5,214 lines: grouped-attention.cu plus
fused_mma.h, fused_mha.h, flash_v100_traits.cuh, fp8_kv_utils.cuh and
paged_kv_utils.cuh) and is compiled into _vllm_fa2_C by the same
target_sources pattern sm70_v37 already uses, with the flags the operator was
qualified with. Only its registration changed: PYBIND11_MODULE becomes
TORCH_LIBRARY_FRAGMENT plus a thin adapter, because a TORCH_LIBRARY schema
spells float arguments as double. The numeric body is untouched. torch/
extension.h is dropped with it, since nothing in the file needs pybind11 or
Python.h once registration moves.
long_attention_enabled() no longer asks whether an environment variable is
set; it asks whether the operator is compiled into the shipped extension.
The manifest variable stays as an explicit override for an unqualified
candidate, and VLLM_SM70_E4M3_LONG_ATTENTION=0 turns the route off, which is
what makes a paired control arm possible from one build and gives operators a
rollback that does not need a rebuild.
MAX_CONTEXT becomes 262152 so the route covers the full capacity plus one page
of generation headroom. The contract no longer imposes a ceiling on the
declared context: the default already equals MAX_CONTEXT, so the check was
always true, and an oversized declaration only fails to equal a real
descriptor bucket and falls back on its own. What the operator really requires
is the query-row range, which mirrors the native q.size(0) in [2, 8] check and
is still enforced.
Measured on four V100-SXM2-32GB, CUDA 12.8, Torch 2.10.0+cu128, QUASAR
Qwen3.8-27B-NVFP4 with seven-draft DFlash2, TP4/B1, E4M3 target KV, FP32
state/logits, no manifest variable set, official context-cost client with
--reset-prefix-cache-before-length and --require-native-prefill:
context round ms before speedup decode tok/s before
32768 18.42 25.57 1.39x 209.7 151.1
65536 20.50 34.74 1.69x 236.2 139.4
131072 24.39 52.91 2.17x 177.2 81.7
261888 38.24 94.93 2.48x 133.4 53.5
Prefill is unchanged (4082/3624/3021/2233 tokens/s), so the route only affects
the long-context attention path. The service log confirms the shipped operator
was selected with no environment variable present.
Paired token/acceptance validation across independent startups is not part of
this change and is being run separately; acceptance lengths above are from the
same client and match the frozen control exactly.
Assisted-by: DeepSeek Harness
Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
yangzhuxinyzx
added a commit
that referenced
this pull request
Sep 12, 2026
…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>
rapatel0
pushed a commit
to rapatel0/1Cat-vLLM
that referenced
this pull request
Sep 12, 2026
…xt fences The grouped E4M3 FP32 long-context operator entered the tree in 1CatAI#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. 1CatAI#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 1CatAI#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 1CatAI#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.
rapatel0
pushed a commit
to rapatel0/1Cat-vLLM
that referenced
this pull request
Sep 12, 2026
The long-context route only runs on page sizes the shipped operator has compiled specializations for. The host entry instantiates exactly 1648 and 3296, and the wrapper mirrored that as a bare tuple literal, so a configuration that landed outside it degraded to the ordinary path without saying anything -- the same silent-failure shape that cost the q1 verifier tail its graph in 1CatAI#602. ADMITTED_PAGE_SIZES now names that set next to the reason it exists, and a declined call reports the page size it saw, once per value, so the next time a window lands outside the compiled set the log says so instead of the number quietly going up. The compact scalar kernel bakes its page geometry in at build time, so its 3296 is named as SCALAR_PAGE_SIZE for the same reason. Measured across three served windows on the reference environment. The KV page size is 3296 in all of them, so the route is admitted in all of them: window request round ms unaccelerated reference 262144 261888 32.8 38.2 131072 130816 26.0 (25.99-26.10) 52.9 65536 65280 23.6 (23.57-24.16) 34.7 Assisted-by: DeepSeek Harness Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
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.
问题 / Problem
SM70 的长文补偿 attention(E4M3 FP32 grouped)此前只以私有 DSO 形式存在,靠
VLLM_SM70_E4M3_LONG_ATTENTION_MANIFEST指向一个外部构建的.so才能启用;而且图上界停在 132096,覆盖不到 262144 的服务容量。结果是:加速路径要手工开启、要额外维护一个版本绑定的二进制产物,256K 完全用不上。#587 当初把这批优化默认关闭(正文写明 "Targets remain unmet; all experimental defaults remain disabled"),所以 main 上跑出来的是未加速的数字。
改动 / Changes
1. 算子源码进仓库
从私有 candidate 搬入
csrc/attention/sm70_grouped_long/,共 5,214 行:2. 接进主构建
按
sm70_v37已验证的target_sources模式编入_vllm_fa2_C,带上该算子被验证时用的编译参数(-gencode=arch=compute_70,code=sm_70 -O3 -std=c++17 --use_fast_math等),并保留那条关键的作用域修正(TARGET_DIRECTORY _vllm_fa2_C,否则 SM70 选项会被静默丢弃)。只改了注册方式:
PYBIND11_MODULE→TORCH_LIBRARY_FRAGMENT(_vllm_fa2_C)+ 一层薄适配(TORCH_LIBRARYschema 的float对应 C++double,故适配层做类型转换)。数值代码一行未动。 同时删掉因此不再需要的torch/extension.h(它拉进 pybind11 需要Python.h,而该 target 的 nvcc 编译不带 Python include 路径)。3. 默认可用
判据从「环境变量是否设置」改为「算子是否编进随包发布的扩展」。manifest 变量保留为实验覆盖(做 A/B 用),新增
VLLM_SM70_E4M3_LONG_ATTENTION=0/false/no/off作为显式关闭——它使同一构建能跑出 control 臂,也给运维一个不需要重编的回退手段。4. 覆盖到 262144 容量
MAX_CONTEXT132096 → 262152(容量 + 一页 generation headroom)。同时去掉契约里对声明上界的封顶:该检查的默认值就等于
MAX_CONTEXT,所以恒为真;而声明过大只会导致attention_context_bucket == context_limit不匹配、自动回退全上下文路径,是安全降级而非故障。真正有约束力的是 query-row 范围,它对应算子 C++ 侧q.size(0) >= 2 && q.size(0) <= 8,仍然保留强制。实测 / Evidence
4×V100-SXM2-32GB,CUDA 12.8,Torch 2.10.0+cu128,QUASAR Qwen3.8-27B-NVFP4 + 7-draft DFlash2,TP4/B1,E4M3 target KV,FP32 state/logits。
未设置任何环境变量(
ENV_CHECK manifest=[<unset>]),服务日志确认选中内置算子:官方 context-cost 客户端,
--reset-prefix-cache-before-length --require-native-prefill:prefill 无回退(4082 / 3624 / 3021 / 2233 tok/s,与改动前一致),说明该算子只影响长文 attention 路径。
未覆盖的部分 / Not covered
MAX_CONTEXT提升后在大容量下的显存开销,尚未做多配置回归。