Skip to content

[Perf] Ship the SM70 grouped long-context route on by default - #602

Merged
yangzhuxinyzx merged 1 commit into
mainfrom
codex/sm70-grouped-long-default-on
Sep 12, 2026
Merged

yangzhuxinyzx merged 1 commit into
mainfrom
codex/sm70-grouped-long-default-on

Conversation

@yangzhuxinyzx

Copy link
Copy Markdown
Contributor

问题 / 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 行

kernel/grouped-attention.cu      4,316
include/fused_mma.h                441
include/fused_mha.h                173
kernel/flash_v100_traits.cuh       118
kernel/fp8_kv_utils.cuh            105
kernel/paged_kv_utils.cuh           61

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_MODULETORCH_LIBRARY_FRAGMENT(_vllm_fa2_C) + 一层薄适配(TORCH_LIBRARY schema 的 float 对应 C++ double,故适配层做类型转换)。数值代码一行未动。 同时删掉因此不再需要的 torch/extension.h(它拉进 pybind11 需要 Python.h,而该 target 的 nvcc 编译不带 Python include 路径)。

3. 默认可用

def long_attention_enabled() -> bool:
    if os.environ.get(DISABLE_ENV, "").strip().lower() in DISABLE_VALUES:
        return False
    return bool(os.environ.get(MANIFEST_ENV)) or builtin_long_attention() is not None

判据从「环境变量是否设置」改为「算子是否编进随包发布的扩展」。manifest 变量保留为实验覆盖(做 A/B 用),新增 VLLM_SM70_E4M3_LONG_ATTENTION=0/false/no/off 作为显式关闭——它使同一构建能跑出 control 臂,也给运维一个不需要重编的回退手段。

4. 覆盖到 262144 容量

MAX_CONTEXT 132096 → 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>]),服务日志确认选中内置算子:

shipped SM70 E4M3 q8 long-context route (sm70_grouped_long_fwd): max_context=262152 query_rows=(8,)

官方 context-cost 客户端,--reset-prefix-cache-before-length --require-native-prefill

context round ms 之前 加速 decode tok/s 之前
32,768 18.42 25.57 1.39x 209.7 151.1
65,536 20.50 34.74 1.69x 236.2 139.4
131,072 24.39 52.91 2.17x 177.2 81.7
261,888 38.24 94.93 2.48x 133.4 53.5

prefill 无回退(4082 / 3624 / 3021 / 2233 tok/s,与改动前一致),说明该算子只影响长文 attention 路径。

未覆盖的部分 / Not covered

  • 配对验证(三次独立启动 + seeds 0/1/2 自然输出,token ID / acceptance / EOS 必须完全一致)尚未完成。 本次只做了单次启动的 round 与 prefill 测量,acceptance 与冻结 control 一致(5.120 @261888 等),但不构成跨启动的配对证据。[Perf] Reduce DFlash2 long-context verification decay #587 当初默认关闭正是因为这批验证没跑完——我们接受把默认开启前移,但要把这个缺口明确标注出来,验证会以独立 PR 或后续 commit 补齐。
  • 回退开关、以及 MAX_CONTEXT 提升后在大容量下的显存开销,尚未做多配置回归。

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
yangzhuxinyzx merged commit ae75fb9 into main Sep 12, 2026
3 of 4 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant