Skip to content

[Feature] Reuse LMDeploy KPool kernels in GLM-5.3 flash indexer - #2269

Open
qescccczmr wants to merge 7 commits into
InternLM:mainfrom
qescccczmr:feat/glm53-lmdeploy-kpool
Open

qescccczmr wants to merge 7 commits into
InternLM:mainfrom
qescccczmr:feat/glm53-lmdeploy-kpool

Conversation

@qescccczmr

@qescccczmr qescccczmr commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Root Cause

XTuner's GLM-5.3 flash KPool indexer used BF16 pool scoring while LMDeploy uses rotated FP8 pools/queries and DeepGEMM MQA scoring. Different precision boundaries and selected-group/tail ordering can change the sparse attention inputs and contribute to rollout/training mismatch.

Fix

  • Reuse LMDeploy's decode compression, query rotation/FP8 quantization, MQA scoring, group selection and token expansion for supported GLM-5.3 geometry: dimension 128, 32/64/128 heads, pool size 4, Top-K 2048/8192.

  • Preserve indexer_backend="tilelang" and existing function names. Check GPU/API availability at forward time and retain the original implementation/projection arithmetic when optional LMDeploy/DeepGEMM dependencies or supported hardware are unavailable. Alternative geometries/scorers/selectors and SP query balancing retain their previous path.

  • Match FP32 key normalization and head-gate projection, explicitly disabling autocast for the latter via fp32_linear.

  • Gather keys/gates before pooling under SP, retain local query ownership/global token IDs, and preserve serving group order and compact causal tails.

  • Chunk query rotation/quantization and bound logits storage using the active MQA API's actual layout.

  • Keep this adapter forward-only for the frozen DSA indexer. Remove the unused differentiable compression API, custom STE backward and gradient-only tests. The actual _compress_storage and _kpool_topk_indices path remains in place.

  • Keep operator contracts in the existing KPool docstrings and experimental evidence in this PR description. The final diff consists of three implementation files and two operator tests, using the existing source/test directories.

KPool precision switch

Set XTUNER_GLM53_KPOOL_BF16 before training workers start, including propagation through the Ray runtime environment when applicable:

Value XTuner KPool behavior
Unset / 0 Use the LMDeploy FP8 adapter for supported geometry/hardware/APIs; retain the existing automatic fallback.
1 Keep BF16 pools and TileLang selection. On supported SM90+ devices with the companion LMDeploy BF16 compression interface, use serving compression with FP32 intermediates and FP32 normalization/head projection. Unsupported devices/dependencies retain the legacy fallback.

Cached precision-specific availability checks keep model preparation and pooling consistent. The BF16 scorer remains TileLang; the FP8 path retains the serving adapter. Keep indexer_backend="tilelang"; no backend renaming or extra model configuration is needed. Restart workers to change the value. MoE-only FP8 is unaffected.

This is an XTuner-only switch. The BF16/BF16 experiment below additionally used a separate LMDeploy BF16 cache/scorer patch with LMDEPLOY_GLM53_KPOOL_BF16=1; that inference-side patch is not part of this XTuner PR. Setting only the XTuner variable does not make a default FP8 rollout engine BF16.

BF16 pooling/head precision follow-up

At 354a1fd6, the BF16 override reuses LMDeploy decode compression rather than rounding probabilities and products to BF16 before pooling. It also promotes head weights before the projection through the existing autocast-safe fp32_linear. No backend or environment variable was renamed. The frozen indexer still has no compression backward.

Paired with LMDeploy #5115 (1c52dae9), a fresh same-input replay on 10.102.215.14 / H200 reproduced:

Check Result
Fixed valid causal score slots 32,632
NaN/Inf in valid slots 0
BF16 pool / FP32 head-weight differences 0 / 0
Score relative L2 difference 2.2019015228460665e-7
Maximum absolute score difference 3.814697265625e-5
Different Top-K sets 0 / 32 queries
Same-side repeated pool/score computation bitwise equal

Inputs came from a captured 4,096-token real-checkpoint prefix; both arms consume identical tensors. Causal slots were fixed from sequence metadata before comparison, excluding 136 expected future -inf entries rather than intersecting finite outputs. The residual is FP32 scorer reduction order, not a claim of bitwise scores or end-to-end KL at e-7.

Fresh follow-up checks: XTuner 62 passed / 6 skipped with the BF16 option, two-GPU SP 2 passed across BF16/FP8, and LMDeploy precision tests 29 passed. Skips cover compact MQA unavailable in the installed wheel and FP8-route-only cases under the BF16 override. Changed-file Ruff and syntax checks pass. Default FP8 compression keeps its existing call form.

Usage: install both PRs into their corresponding environments, preserve indexer_backend="tilelang" and freeze_dsa_indexer=True, and export both startup variables before launching/restarting all training and rollout workers:

export XTUNER_GLM53_KPOOL_BF16=1
export LMDEPLOY_GLM53_KPOOL_BF16=1

The complete MoE-only recipe additionally uses FP8=1 ROLLOUT_FP8=1 LMDEPLOY_FP8_MOE_ONLY=1; our current diagnostic recipe also keeps LMDEPLOY_MLA_DISABLE_DENSE_PREFILL=1. Propagate these through Ray runtime environments. The existing SwiGLU/mHC patches are separate from this PR. A whole-model mismatch/mismatch_k3_kl <= 2e-4 remains an end-to-end validation target, not a result established by this operator replay.

Review: legacy MQA allocation budget

Addresses the legacy-budget review: compact columns use align(N, 256); legacy absolute columns use align(N + 256, 64), including the guard columns. Both APIs pad query rows to 128 / heads. Chunking now accounts for the selected layout rather than applying the compact formula to both.

The regression checks logits.untyped_storage().nbytes(), including short unaligned chunks. For 4096 tokens / 1024 pools / 32 heads / a 1-MiB budget, the old 256-row chunk allocated 1,310,720 bytes. The corrected 204-row chunk allocates 1,044,480 bytes, within 1,048,576 bytes.

Scope

This reuses serving operators; it does not claim complete prefill/decode scheduling or full-model log-probability parity. Training compresses closed pools with mode="decode", while serving initially builds prompt pools with online extend compression. SequenceContext lacks the prompt/chunk history needed to reproduce that schedule. The adapter uses internal LMDeploy helpers verified at 3228395546dfb2f6427faf39ec0c0c285f4c378a.

Top-K IDs remain nondifferentiable, the indexer stays frozen, and freeze_dsa_indexer=False remains unsupported. This PR adds no compression backward or end-to-end indexer training. Existing attention/MoE backward is unchanged. It contains no mHC, KDA QKV, SiLU or MoE W2 modifications.

Test Plan

  • Precision-switch follow-up: fresh CPU/GPU regressions on the requested H200 host: 64 passed / 2 skipped with FP8 enabled, and 59 passed / 7 skipped with the BF16 override. The latter skips five FP8-route-only cases plus two unavailable compact-MQA cases. Tests verify that BF16 bypasses optional CUDA/dependency probes and preserves legacy model preparation plus selector dispatch, with CPU/CUDA autocast on/off.

  • Public-selector tests that specifically require LMDeploy FP8 now check the enabled/available path, including the empty-local-query fixture. This avoids running that fixture through an incompatible fallback. Changed-file pre-commit and Ruff pass; mypy checks all 431 source files.

  • Layout/docstring follow-up at ffe21b9d: executable AST unchanged relative to 79d33b3d; the renamed tests/ops/test_lmdeploy_kpool_sequence_parallel.py passes a fresh two-GPU check. That revision collected 61 cases under the final filenames; the precision-switch follow-up now collects 66.

  • H200 CPU/GPU regression at 79d33b3d: 59 passed, 2 skipped, including two-rank NCCL SP. The skipped cases require compact MQA, which the installed legacy wheel does not expose; CPU layout tests cover both contracts and real-storage GPU tests cover the available legacy API.

  • Pool FP8 values/scales match the serving operator bitwise; independent PyTorch forward comparisons cover both compression modes and scale settings. Empty groups, architecture fallback, CPU/CUDA autocast, dependency fallback, packed causality and group/tail order remain covered.

  • Query FP8 payloads and folded scale weights are checked at the actual MQA call. Valid causal logits are checked against independent Torch FP8 scoring using DeepGEMM's simulated-FP8 normalized-error criterion (5e-6).

  • Fixed inputs, seed 123, packed lengths (8195, 8189), heads32/dim128/Top-K2048: zero changed token IDs versus 75aeaec1 at row caps 1024/257 and logits budgets 512/1 MiB.

  • Changed-file pre-commit, explicit Ruff and mypy across 431 source files pass. Full-repository pre-commit also ran in an isolated checkout; seven untouched autotest/ files have existing formatting/import issues, excluded from this change.

Recorded MoE-only GRPO check

The following three-step A/B results were measured at 75aeaec1, before removing the unused backward, against pre-adapter baseline c93eb15d. Both arms ran on the same 8×H200 host with the same five-layer checkpoint, seed 123, EP4/SP1 training, TP1/EP4 rollout and 64 responses per step. Attention/dense GEMMs remained BF16, grouped MoE GEMMs used tilewise FP8, and serving's effective quantization scope was moe_only.

Step MoE-only baseline MoE-only + KPool adapter
1 3.7338290713e-4 3.3972592699e-4
2 3.6995386472e-4 3.4364513704e-4
3 3.7097674413e-4 3.4235174497e-4
Mean 3.7143783866e-4 3.4190760300e-4

Mean mismatch/mismatch_k3_kl was 7.95% lower. Current forward parity against that measured commit is checked above. These independently generated trajectories are not a fixed-token causal ablation; rewards/losses/gradient norms were zero, so this is numerical smoke-test evidence, not evidence of learning or model-quality gains.

Recorded BF16/BF16 versus FP8/FP8 MoE-only GRPO check

A separate fresh two-arm check on 2026-11-09 used the same 1650 / 8×H200 host, five-layer checkpoint, seed 123, EP4/SP1 training, TP1/EP4 rollout, 64 responses per step and length limits 2048/14336/16384. The DSA indexer stayed frozen; grouped MoE GEMMs remained tilewise FP8, ordinary attention/dense GEMMs remained BF16, and serving's effective quantization scope was moe_only. No mHC, KDA QKV or SiLU/W2 patches were enabled.

Both arms used ffe21b9d plus this startup gate and the same LMDeploy 32283955 source with the MoE-only correction and the separate BF16-selection patch. The three implementation files in this follow-up have the same executable AST as that measured experiment after removing docstrings; this is recorded evidence rather than a claim of another GRPO run on the final commit.

Step XTuner BF16 / LMDeploy BF16 XTuner FP8 / LMDeploy FP8
1 3.1074427909e-04 3.3745854612e-04
2 3.1347083859e-04 3.4436648564e-04
3 3.1139212660e-04 3.4095671388e-04
Mean 3.1186908143e-04 3.4092724855e-04

For BF16/BF16 both XTUNER_GLM53_KPOOL_BF16 and LMDEPLOY_GLM53_KPOOL_BF16 were 1; for FP8/FP8 both were 0. Each arm completed all three steps with exit code 0 and 192 responses. The actual environments of eight training and eight inference workers per arm were checked. Serialized training configurations and source revisions/worktrees matched; recorded settings differed only in the two precision variables and output directory.

BF16/BF16 had 8.5233% lower mean mismatch/mismatch_k3_kl in this run, while both remained at the e-4 level. This compares complete KPool pipelines: projection precision, pooling accumulation and group/tail ordering also differ, so the result cannot be attributed solely to FP8 quantization. The trajectories were independently generated, not paired fixed-token inputs. Rewards, losses and gradient norms were zero; three steps on a reduced model do not establish learning or full-model quality gains. The default remains FP8 to match an unmodified FP8 rollout engine; the switch preserves the BF16 option for deployments that configure both sides accordingly.

Upstream CI status is tracked by the PR checks.

Five-step combined numerical check (2026-11-11, H200)

A fresh five-step check completed on 10.102.215.14 / 8 x H200, exit code 0, using the five-layer reduced GLM-5.3 Flash 25B checkpoint, GSM8K, EP4/SP1 training and TP1/EP4 rollout, prefix caching and MTP with num_speculative_tokens=5.

This is a combined recipe: BF16 KPool pooling/head alignment, FP8 MoE activation rounding, opt-in FP32 KDA chunks, and the separate mHC-post/SwiGLU alignment patches. The eight startup variables FP8, ROLLOUT_FP8, LMDEPLOY_FP8_MOE_ONLY, XTUNER_GLM53_KPOOL_BF16, LMDEPLOY_GLM53_KPOOL_BF16, XTUNER_GLM53_LMDEPLOY_SWIGLU, XTUNER_GLM53_KDA_FP32, and LMDEPLOY_MLA_DISABLE_DENSE_PREFILL were all 1. The actual environments of 8 training workers, 8 inference-compute workers and their engine/controller wrappers were checked; the prefix-cache replay probe also passed.

Step mismatch/mismatch_k3_kl
1 2.6386266109e-04
2 2.6337933377e-04
3 2.6362264180e-04
4 2.6350014377e-04
5 2.6471854653e-04
Mean 2.6381666539e-04

The end-to-end <=2e-4 target was not reached (all five values remain above it). TensorBoard scalar values were cross-checked with the worker's full-precision log values. Local KPool score relative L2 at 2.2e-7 is a different metric and does not imply whole-model KL at that level.

Rewards, losses and gradient norms were zero; this is a numerical GRPO smoke test, not evidence of useful learning or nonzero parameter updates. No per-patch causal improvement is inferred from this combined run. Remaining differences, including the actual DeepEP dispatch/combine path and KDA recurrence, are still under diagnosis.

@qescccczmr qescccczmr changed the title [Fix] Align GLM-5.3 KPool with LMDeploy kernels [Fix] Reuse LMDeploy KPool kernels in GLM-5.3 indexer Nov 6, 2026
RunningLeon

This comment was marked as off-topic.

@qescccczmr
qescccczmr marked this pull request as ready for review November 9, 2026 06:51
@qescccczmr qescccczmr changed the title [Fix] Reuse LMDeploy KPool kernels in GLM-5.3 indexer [Fix] Reuse LMDeploy KPool kernels in GLM-5.3 flash indexer Nov 9, 2026
@qescccczmr

Copy link
Copy Markdown
Collaborator Author

PR #2269 自查

summary

  • 审查对象:PR #2269,head ffe21b9d9956573a31e07098616a71de2b74e08d;target main 098be1956e3bf171f08d697d93db70b965f2e51a。实际 PR diff 的 merge-base 为 c93eb15dc0b1e4fd371b33d79bec9e1321fe20f2,下图的 before 对应该基线,并非只比较最后一次文档整理。
  • 范围:3 个实现文件、2 个 operator 测试文件。依据用户提供的 review_helper.md,采用 improve-codebase-architecture 和 codebase-design 的 Module、Interface、Depth、Seam、Adapter、Leverage、Locality 及 deletion test。仓库未发现已跟踪的 GLOSSARY.md 或 docs/adr/,未新增术语表或类层级。
  • 自查结论:REQUEST_CHANGES。 在已说明的冻结 Indexer、BF16 输入、均匀 SP、兼容 H200/LMDeploy 配置下,未发现新的 P0/P1 数学或并发错误;但 T1 是确定的 P2 测试兼容性缺陷,A1 按本次要求未满足 R2/R3。不能将该设计检查写成全部通过。
  • 本轮以静态审查和独立交叉复核为主。仅为核实 T1,在指定远端执行一次 CPU 最小复现,CUDA_INITIALIZED=False;未重跑 GRPO 或完整 GPU suite,也未修改本次审查的实现。

Main Flowchart before this PR

以下展示目标配置 indexer_backend=tilelang、freeze_dsa_indexer=True 的主要路径;其他 scorer/selector 是原有分支。

graph TD
  subgraph B_CFG["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMLAConfig.build"]
    BC["校验冻结约束并构造 NoPEDSAMultiLatentAttention"]
  end
  subgraph B_INIT["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMultiLatentAttention.__init__"]
    BI["构造 KPoolIndexer;冻结其参数"]
    BA["xtuner.v1.ops.sparse_mla.get_sparse_mla"]
  end
  subgraph B_KINIT["xtuner.v1.model.moe.glm53.nope_dsa_mla.KPoolIndexer.__init__"]
    BK["xtuner.v1.ops.sparse_mla.get_kpool_topk_indices:解析 selector callable"]
  end
  subgraph B_SEQ["xtuner.v1.data_proto.sequence_context.SequenceContext.split"]
    BS["保留全局 cu_seq_lens;记录本 shard 起点"]
  end
  subgraph B_ATTN["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMultiLatentAttention.forward"]
    BF["计算 absorbed Q/KV 与 q_resid"]
    BN["torch.no_grad 内调用 self.indexer"]
    BM["self.sparse_mla_func 使用 int32 token IDs"]
    BO["value/output projection;返回 AttnOutputs"]
  end
  subgraph B_KFWD["xtuner.v1.model.moe.glm53.nope_dsa_mla.KPoolIndexer.forward"]
    BP["wq_b;wk + k_norm;compression gate"]
    BW["weights_proj 在原 dtype 投影,结果再转 FP32"]
    BT["调用 self._topk_indices_fn"]
  end
  subgraph B_OP["xtuner.v1.ops.sparse_mla.kpool.kpool_topk_indices"]
    BG["gather_for_sequence_parallel:先 gather K/gates"]
    BB["build_pools:每文档 weighted pooling,输出原 dtype"]
    BR["pool_causal_ranges:全局 pool 范围"]
    BQ["tilelang_indexer_topk_from_ranges:BF16 score / Top-K"]
    BE["expand_pools_and_tail:排序 pool,固定列放 tail,再 pad"]
  end
  BC --> BI
  BI --> BK
  BI --> BA
  BK --> BF
  BS -.-> BF
  BF --> BN
  BN --> BP
  BP --> BW
  BW --> BT
  BT --> BG
  BG --> BB
  BB --> BR
  BR --> BQ
  BQ --> BE
  BE --> BM
  BA -.-> BM
  BF -.-> BM
  BM --> BO
Loading

原类/权重归属、SequenceContext 的全局坐标约定、冻结约束及 attention backward 均为既有行为。

Main Flowchart after this PR

图示的是 已提交代码。黄色为新增或修改节点,红色为 A1 涉及的模型类内后端分叉;不是将建议重构画成已完成。使用 graph TD、subgraph、classDef;两张图均已通过 Mermaid 8.8.0 parser 语法验证。

graph TD
  subgraph A_CFG["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMLAConfig.build"]
    AC["冻结约束与 attention 构造保持原样"]
  end
  subgraph A_INIT["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMultiLatentAttention.__init__"]
    AI["构造 KPoolIndexer;冻结其参数"]
    AA["xtuner.v1.ops.sparse_mla.get_sparse_mla"]
  end
  subgraph A_KINIT["xtuner.v1.model.moe.glm53.nope_dsa_mla.KPoolIndexer.__init__"]
    AK["保留 get_kpool_topk_indices 与 tilelang 名称"]
    AE["新增 _uses_lmdeploy_kpool:geometry / backend / balance 判断"]
  end
  subgraph A_SEQ["xtuner.v1.data_proto.sequence_context.SequenceContext.split"]
    AS["保留全局 cu_seq_lens;记录本 shard 起点"]
  end
  subgraph A_ATTN["xtuner.v1.model.moe.glm53.nope_dsa_mla.NoPEDSAMultiLatentAttention.forward"]
    AF["计算 absorbed Q/KV 与 q_resid"]
    AN["torch.no_grad 内调用 self.indexer"]
    AM["self.sparse_mla_func 使用 int32 token IDs"]
    AO["value/output projection;返回 AttnOutputs"]
  end
  subgraph A_KFWD["xtuner.v1.model.moe.glm53.nope_dsa_mla.KPoolIndexer.forward"]
    AP["计算 Q、projected K 与 gate 参数"]
    AV{"_uses_lmdeploy_kpool 且 runtime 可用?"}
    AP32["FP32 layer_norm 后 cast;fp32_linear 禁用 autocast"]
    APOLD["原 k_norm 与 weights_proj 路径"]
    AT["调用同一个 self._topk_indices_fn"]
  end
  subgraph A_OP["xtuner.v1.ops.sparse_mla.kpool.kpool_topk_indices"]
    AD{"再次检查 geometry / runtime / scorer / balancing"}
    AOLD["原 build_pools + TileLang + expand_pools_and_tail"]
  end
  subgraph A_LMD["xtuner.v1.ops.sparse_mla.lmdeploy_kpool._kpool_topk_indices"]
    AG["全局 gather K/gates;build_pool_index"]
    AB["_compress_storage:LMDeploy decode compression + FP8 values/scales"]
    AR["pool_causal_ranges;_score_chunk_size 计算真实 padded storage 上限"]
    AQ["分块 rotate_kpool_query / FP8 quantize;合并 head 权重与 scales"]
    AL["DeepGEMM.fp8_mqa_logits;kpool_select_groups_cuda"]
    AX["保留 group 顺序;compact tail;加文档 offset;pad int32 IDs"]
  end
  AC --> AI
  AI --> AK
  AI --> AA
  AK --> AE
  AE --> AF
  AS -.-> AF
  AF --> AN
  AN --> AP
  AP --> AV
  AV -->|是| AP32
  AV -->|否| APOLD
  AP32 --> AT
  APOLD --> AT
  AT --> AD
  AD -->|LMDeploy| AG
  AD -->|fallback| AOLD
  AG --> AB
  AB --> AR
  AR --> AQ
  AQ --> AL
  AL --> AX
  AX --> AM
  AOLD --> AM
  AA -.-> AM
  AF -.-> AM
  AM --> AO
  classDef changed fill:#fff3cd,stroke:#b58100,stroke-width:2px;
  classDef review fill:#ffe8e8,stroke:#cb2431,stroke-width:2px;
  class AP32,AD,AG,AB,AR,AQ,AL,AX changed;
  class AE,AV review;
Loading

代码对应:模型投影位于 KPoolIndexer.forward;新增路由位于 kpool_topk_indices;LMDeploy adapter 的 gather、压缩、分块打分及 token expansion 位于 _kpool_topk_indices。红色节点在下文按设计标准评估。

核心原理

  1. 池化与精度边界。 每个文档单独按 4 tokens 分组,压缩权重为 softmax_slot(gate + APE)。LMDeploy 负责 decode 两遍累加、BF16 rounding、归一化 Hadamard 和 E4M3FN 量化;adapter 使用其 values/scales,未再实现一套压缩算法。Query 同样旋转和量化。
  2. Scoring 缩放只应用一次。 query_weight = head_projection_fp32 × heads^(-1/2) × query_scale × head_dim^(-1/2);MQA 对逐 head 的 ReLU(dot(query_fp8, pool_fp8)) 加权求和,再应用 pool scale。FP8 payload/scale 权重要求逐位一致;Torch 与 Tensor Core logits 用受控数值误差比较,不能要求任意 FP32 归约逐位一致。
  3. 因果与 SP。 K/gates 必须在池化前 gather,避免跨 shard 的一个池缺少 token。Query 保留本 rank 所有权;可见 complete pools 是文档内的前缀,尾部未满 4 的因果 tokens 单独追加,最后转换为全局 token IDs。
  4. 顺序也影响数值。 原路径排序 pools、固定位置存 tail;LMDeploy 路径保留选中 group 顺序并紧凑放 tail。这会改变 attention 归约顺序,因此不能仅检查选中 token 的集合相同。
  5. Logits 预算按 backing storage。 令 bq=128/heads,compact stride 为 align(N,256),legacy stride 为 align(N+256,64)。实际大小为 ceil(M/bq) × bq × stride × 4 bytes。1 MiB、1024 pools、32 heads 下,legacy 旧 256-row chunk 分配 1,310,720 bytes;204-row chunk 分配 1,044,480 bytes,满足预算。这约束 logits,不是整个 operator 的峰值显存。
  6. 范围限制。 Indexer 参数仍冻结、Top-K 为整数、选择在 no_grad 下执行;没有新增 Compression Backward。训练闭合池统一使用 decode,推理 prompt 初始池使用 extend。现有输入不携带完整 prompt/chunk 历史,因此不能宣称完整 serving schedule 或全部 LogProb 一致;这是已公开的范围限制,不重新列成未披露的 P1。

核心实现与对应的单测

实现 / 代码 对应测试 证明范围
KPoolIndexer.forward 的 FP32 head projection test_backend_registration_and_fp32_projection CPU/CUDA、autocast 和 fallback 下 head 权重的 dtype/数值;key norm 尚缺独立断言,见 T2
geometry/依赖探测与 fallback test_public_selector_preserves_original_path_without_lmdeploy 等 验证缺少 LMDeploy 与低架构时的路由;不代表穷尽所有部分安装,见 C1
_compress_storage TestKPoolCompression 空池、非法输入、两种 mode、scales、bitwise 封装一致与独立 Torch 参考;直接比较同一算子的测试只是集成验证
Query 量化与权重折叠 test_query_quantization_and_mqa_logits 实际传给 MQA 的 payload/scaled weights,以及有效 causal logits 的 normalized error ≤ 5e-6
_score_chunk_size test_logits_budget_bounds_actual_storage 与 预算单测 检查 underlying storage,包含 guard、M padding 和短 chunk;compact 的真实 GPU 两例在现有 wheel 上跳过
group order / compact tail test_preserves_lmdeploy_group_order_and_compact_tails 故意逆序 group,检验顺序和 tail 列位置
packed causal ranges 与全局 IDs test_packed_selection_is_causal_and_matches_fp8_scores 不跨文档、不选未来 token、无重复,选中池满足参考分数阈值
先 gather 后 pooling test_two_gpu_gathering_and_packed_ownership 两卡跨 seam、文档边界、全局 IDs、本地 query 所有权及超过 Top-K 的上下文

抽象与信息隐藏评估

总体:operator Module 已具有实际 Depth,但 Indexer 的数值准备与后端选择尚未形成一个完整 Seam。 tilelang 名称和返回 Interface 保持稳定;没有新增类层级,删除未使用的可微压缩 Module 也符合 deletion test。

Module / Interface deletion test 与评估
lmdeploy_kpool._kpool_topk_indices 隐藏压缩、scoring、扩展的多项真实规则;删除后规则会散回调用方,有保留价值。原实现和 LMDeploy 是两个真实 Adapter,Seam 不是为假想需求引入
_compress_storage 包含空输出、输入约束、架构派发及 CPU 参考路径;并非无行为的一行转发,保留 private Interface 合理。CPU/extend 的测试覆盖不等于生产 CUDA/decode 路径的全部覆盖
_score_chunk_size 隐藏版本相关的 padding 与字节预算,具有 Locality 和 Leverage;真实 allocation 单测比重复计算公式更有价值
_supports_lmdeploy_kpool 虽是一行,但集中维护真实 geometry allowlist;不能因行数短就机械删除
KPoolIndexer 与 kpool_topk_indices A1(架构改进,Strong):同一次 Adapter 决策跨两个 Module 重复出现,precision 规则泄漏到既有类。 今天正常 factory 调用的条件一致,未证明存在实际路由错误;风险是后续修改需要同步维护多处规则

A1 的解决方向:把 numerical preparation 与 selection 收敛在同一深 Module 的 Interface 下,复用一个实现选择结果;模型保留参数所有权和 checkpoint key。这样改变 geometry、fallback 或 dtype 规则时,修改与测试能集中,改善 Locality/Leverage。具体伪代码与 R1–R3 对应放在下一节;不增加无必要的抽象基类。

新增功能守则

只评估本 PR 新增接触点。NoPEDSAMultiLatentAttention 的冻结分支和构造约束为基线已有代码,不应误报为本 PR 新污染。

规则 KPoolIndexer 的实际情况 判定
R1:≤1 字段 + 1–2 次具名调用 新增 _uses_lmdeploy_kpool 一个字段;引入两个 eligibility/capability helper,并在新分支中加入 FP32 norm/projection。若只数后端 helper 可达到形式限制,但调用方仍承担真实 implementation 字段上限满足,不能据此判整体零污染
R2:既有类不含新特性的实现细节 forward:124-139 含 FP32 norm、参数 materialization、dtype 恢复与特定投影规则 不满足
R3:既有类不按新特性分叉 forward:124 显式按 LMDeploy eligibility 和 runtime availability 分支 不满足

A1 解决方案伪代码(仅表示职责位置,非已提交接口):

# 既有模型:保留原有参数注册位置与权重 key。
# 以一个 callable 替代旧 selector 字段,无需再保存 feature flag。
self._run_indexer = build_kpool_runner(backend, geometry, options)

def forward(hidden_states, q_resid, seq_ctx):
    q = self.wq_b(q_resid).view(...)
    projected_k = self.wk(hidden_states)
    return self._run_indexer(
        hidden_states, q, projected_k, seq_ctx,
        norm=self.k_norm, head_projection=self.weights_proj,
        compression_parameters=materialized_parameters,
    )

# operator Module 内部:两个真实 Adapter 可用函数组合实现,
# 必要时使用纯数据 record;不要求新增类或迁移模型参数。
def run_indexer(...):
    adapter = resolve_adapter(q.device)  # 同一次决策控制 prepare 与 select
    k, head_weights, gates = adapter.prepare(...)
    return adapter.select(q, k, gates, head_weights, ...)

这里的 adapter 是概念占位。落地时须保留公共 tilelang 名称、fallback 投影语义、DTensor materialization 和 state-dict key,并用公开 Interface 回归验证;不要为了消除一个 if 增加多层空转的 Module。

公开接口的线性业务流程评估

  • NoPEDSAMultiLatentAttention.forward 仍是“准备 Q/KV → Indexer → sparse attention → output projection”的线性流程,本 PR 没有修改它。
  • KPoolIndexer.forward 新增后端条件和底层精度规则,抽象层级混杂;由 A1 收敛,不再额外增加一套转发层。
  • 公共 kpool_topk_indices 的后端 dispatch 是合理 Seam,但其决策应与 projection 共用来源。
  • 新增 _kpool_topk_indices 同时包含通信/layout、打分循环与 token 扩展。当前阶段次序清晰;若继续扩展,可把拥有完整规则的阶段下沉,属于 Worth exploring,不以方法长度本身作为缺陷。

有收益的下沉示意:

def _kpool_topk_indices(...):
    pools = _prepare_pools(...)       # gather + 文档 layout + compression
    groups = _score_and_select(...)  # causal ranges + budget + FP8/scales + MQA
    return _expand_selected(...)    # group order + tail + global offset + padding

deletion test:只有能隐藏上述整组规则、改善 Locality 的 private Module 才值得保留;rotate() 只转发一个现有函数等无规则 wrapper 应内联。相关测试仍应优先通过 kpool_topk_indices 这个 Interface 验证完整流程。

单测建议

T1 — P2:新空 query 测试在 fallback 环境不成立,建议合入前修复

位置:test_empty_local_queries:404-420。它只检查 CUDA,构造了“全局 1 token,query/key 都为 0 tokens,且没有 SP mesh”的夹具,然后调用公共 dispatcher。

CUDA 可用但缺少 LMDeploy,或设备为 SM80 时,dispatcher 合法进入原路径;原 build_pools 的全局长度检查 会因 1 != 0 抛错。该问题来自 新测试的适用条件/夹具,不是有效非空生产输入的回退故障,也不能据此断言 SP 死锁。

已在指定远端以 CPU、强制 availability=False 的最小调用核实同一 fallback 检查:

build_pools needs key features for the whole sequence (1 tokens) but got 0;
gather them across the sequence-parallel mesh before calling.
CUDA_INITIALIZED: False

这验证的是 fallback 控制流和元数据矛盾,未声称已构建“CUDA 有但 LMDeploy 未安装”的完整测试环境。

建议:LMDeploy 专属测试的适用条件应与真实 Adapter 能力相同;公共 Interface 的 empty-input 行为则使用合法全局元数据另行定义。直接执行 MQA 的测试也检查完整能力。现有双卡测试的 guard 可作为写法参照。

# LMDeploy 专属 case;不改变生产 fallback 的语义。
if not cuda_available or not lmdeploy_kpool_available(device):
    pytest.skip("Requires compatible LMDeploy/DeepGEMM and SM90+")
# 保留该 Adapter 的 empty fast-path 检查。
...

# 公共 Interface 的 fallback case:使用与全局 context 一致的 K/gates。
with force_fallback():
    result = kpool_topk_indices(valid_inputs, valid_seq_ctx, ...)
    assert_expected_causal_ids(result)

T2 — key norm 精度的独立断言不足

test_backend_registration_and_fp32_projection 同时捕获 key 和 weights,但只断言 weights。FP32 key norm 是本 PR 的实质修改,应独立覆盖,不能以 head-gate 测试代替。

# 使用非平凡 norm weight/bias;autocast on/off 分别覆盖。
z = projected_keys.double()
mu = z.mean(dim=-1, keepdim=True)
var = ((z - mu) ** 2).mean(dim=-1, keepdim=True)
expected_k = ((z - mu) / sqrt(var + eps) * norm_weight + norm_bias)
assert_bf16_close(captured_keys, expected_k.to(output_dtype))

这是补足 Interface 数值契约的建议,未发现当前 key norm 公式错误;独立高精度参考应允许最终 BF16 rounding 的合理误差。

T3 — 其他覆盖范围

  • 对允许的 64/128 heads、Top-K 8192 增加代表性端到端 case;当前端到端主测为 heads32/Top-K2048,预算 CPU 参数化不等于这些 geometry 的 GPU 计算验证。
  • compact MQA 的真实 GPU storage 两例在记录环境中跳过;应在具备该 Interface 的环境补跑,不能把公式检查当作实际 allocation 证明。
  • SP 可增加 SequenceContext.split 的奇数全局长度/padding 与 4-rank case;已有双卡测试手工构造 metadata,证明其给定范围,未证明所有切分。
  • 核心 numerical 测试若必须穿过 private Interface,应明确其“内部 Seam”的角色;封装 bitwise 测试、独立 Torch 参考与公开入口端到端测试各有作用,不能相互替代。

示意参数表:

@pytest.mark.parametrize("heads,topk", [(32,2048), (64,2048), (128,8192)])
def test_supported_geometry_through_public_interface(heads, topk):
    require_matching_adapter_capability(heads, topk)
    assert_causal_selection_and_reference_scores(...)

其他 issues

C1 — 依赖能力探测尚不完整,属于兼容性加固建议

_lmdeploy_kpool_is_available 检查了一组 operators,但实际执行还导入 _mqa_has_local_columns,并读取 envs.dsa_indexer_max_logits_mb。部分安装或未来私有 Interface 变化可能导致 probe 返回真、执行再失败。

未识别到所声明兼容 LMDeploy revision 的实际失败,不能将其写成已发生的生产回退 bug。 建议与 A1 一起在一个 Module 内解析并验证完整 operator bundle,改善 Locality;缓存 callable/module,不把可变预算值永久冻结。真正 operator 执行异常仍应抛出。

def resolve_lmdeploy_operators(device):
    if not supported_device(device):
        return None
    try:
        operators = import_all_operators_actually_used()
        validate_callable(operators.mqa_layout_probe)
        validate_budget_configuration_present()
        return operators
    except known_dependency_or_import_errors:
        return None

已有证据与范围,不混写成新增通过记录

  • 79d33b3d 的已存远端记录为 59 passed / 2 skipped。ffe21b9d 的变动为文档整理/测试重命名;已有去除 docstring 后 AST 等价检查、改名后双卡 SP 1 passed、61 cases collected、改动文件 pre-commit 与全仓 mypy 431 sources 通过。本轮读取这些证据,没有重新执行整个 suite。
  • 全仓 pre-commit 的七个未改动 autotest/ 文件格式问题为基线问题,不列成本 PR 新问题。当前 PR 不再创建 doc/,最终布局为现有 source/test 目录中的五文件,命名与相邻模块一致。
  • 原生三步 GRPO 数字来自 75aeaec1:同机、同配置、每步 64 responses,均值 3.7143783866e-4 → 3.4190760300e-4,观测降低 7.95%。轨迹独立生成,奖励/loss/grad norm 为 0;这不是 fixed-token 因果消融,也不是本轮或最新 head 的重新训练结果,不能推断有效学习或完整模型质量提升。
  • CI 以 PR checks 为准;没有把尚未完成的 unit-test 流程视为通过。

收尾:本次交付为自查文档和 T1 最小复现物料,未修改被审查的源代码,未新增仓库文档目录。 合入建议仍为:修复 T1;按本次明确的零污染标准收敛 A1,或在设计审查中明确处理该不符合项。其他条目区分为覆盖建议与兼容性加固,没有发现的错误不作结论。

@qescccczmr

Copy link
Copy Markdown
Collaborator Author

Updated in 4385e729.

  • Keep XTUNER_GLM53_KPOOL_BF16 as the startup switch: 1 restores the original preparation and BF16 KPool selection; unset/0 keeps the current FP8 adapter when supported. The model and selector use the same gate, and tilelang is unchanged. Propagate this variable to training workers before startup.
  • Add CPU/CUDA autocast regression coverage and fix the applicability guards of LMDeploy-FP8-only public-selector tests, including the empty-query fixture identified in the self-review.
  • Fresh H200 regression: 64 passed / 2 skipped in FP8 mode; 59 passed / 7 skipped with the BF16 override. BF16 mode skips five FP8-route-only cases; two compact-MQA cases are unavailable in this environment. Changed-file pre-commit, Ruff and mypy across 431 source files pass.

The PR description now includes the fresh three-step MoE-only comparison on the same 1650 / 8×H200 host:

Step XTuner BF16 / LMDeploy BF16 XTuner FP8 / LMDeploy FP8
1 3.1074427909e-4 3.3745854612e-4
2 3.1347083859e-4 3.4436648564e-4
3 3.1139212660e-4 3.4095671388e-4
Mean 3.1186908143e-4 3.4092724855e-4

BF16/BF16 had 8.5233% lower mean mismatch/mismatch_k3_kl in this run. The BF16 rollout used a separate LMDeploy BF16-selection patch and LMDEPLOY_GLM53_KPOOL_BF16=1, which are not included in this XTuner PR. Setting only the XTuner switch does not reproduce the BF16/BF16 arm.

These are three-step, independently generated trajectories on a five-layer reduced checkpoint, comparing complete KPool pipelines rather than quantization alone. The indexer stayed frozen and rewards/losses/gradient norms were zero, so this is numerical mismatch evidence, not a learning or model-quality claim. The implementation AST matches the measured precision-switch experiment apart from docstrings; no new GRPO run on this final commit is claimed.

@qescccczmr qescccczmr changed the title [Fix] Reuse LMDeploy KPool kernels in GLM-5.3 flash indexer [Feature] Reuse LMDeploy KPool kernels in GLM-5.3 flash indexer Nov 9, 2026
@Denny991

Copy link
Copy Markdown
Collaborator

上面均在本地复现,且审核代码没有发现问题。

配置 k3_kl 相对基线
基线(MoE-only FP8) 3.729e-4 —
+ PR2269(训练借 FP8 算子) 3.431e-4 -8.0%
+ 03 patch(双侧 KPool BF16) 3.122e-4 -16.3%

@qescccczmr

Copy link
Copy Markdown
Collaborator Author

summary

No blocking findings remain in the KPool follow-up. The BF16 startup option now selects serving compression and FP32 model preparation on supported SM90+ devices; the BF16 TileLang scorer remains. LMDeploy's established arithmetic is unchanged; tests pin it down.

Main Flowchart before this PR

flowchart TD
subgraph XTuner_KPoolIndexer_forward
 A[BF16 override] --> B[BF16 head projection then FP32 cast]
end
subgraph XTuner_kpool_build_pools
 B --> C[FP32 softmax then BF16 probabilities/products]
end
subgraph XTuner_kpool_topk_indices
 C --> D[TileLang score / causal Top-K / token expansion]
end
Loading

Main Flowchart after this PR

flowchart TD
subgraph XTuner_KPoolIndexer_forward
 A[BF16 override + supported serving operators] --> B[FP32 normalization and fp32_linear]
end
subgraph XTuner_kpool_build_pools
 B --> C[LMDeploy decode compression / FP32 intermediates / BF16 store]
end
subgraph XTuner_kpool_topk_indices
 C --> D[TileLang score / causal Top-K / token expansion]
end
style B fill:#d9f2d9,stroke:#267326
style C fill:#d9f2d9,stroke:#267326
Loading

核心原理

BF16 storage does not imply BF16 pooling products or head projection. For matching inputs, keep those computations in FP32 until pool storage. Indexer IDs remain frozen and nondifferentiable; no new compression backward is introduced.

核心实现与对应的单测

_lmdeploy_kpool_is_available(..., bf16=True) checks the selected startup option, hardware and BF16 compression interface without probing DeepGEMM. Model preparation uses the same predicate. build_pools keeps packed document ownership, initializes invalid key slots, masks invalid compression slots, and zeros wholly padded pools. Tests cover complete/partial/padding pools, FP32 projections under autocast, missing dependencies, both precisions and two-rank SP.

On 10.102.215.14 / H200: XTuner 62 passed / 6 skipped (BF16 run), SP 2 passed, LMDeploy precision 29 passed. Compact MQA and FP8-only public paths account for skips. Fixed real-model replay has 32632 predefined valid scores, zero nonfinite values, identical pool/head values and A/A output, score relative L2 2.2019015228460665e-7, maximum absolute error 3.814697265625e-5, and 0/32 different Top-K sets. This is an operator check, not end-to-end KL acceptance.

抽象与信息隐藏评估

The existing adapter module earns its depth: compression, optional dependencies and storage dtype stay at the existing seam. No new production class/module is added. The interface adds a dtype to an existing private helper and a precision-specific availability query, providing locality without moving serving scheduler state into training.

新增功能守则

The existing model has one preparation condition changed; no feature field or numerical implementation is added to it. FP32 projection uses the existing fp32_linear. The numerical details live in the existing pooling adapter. A new class or polymorphic selector would add more concepts than this precision fix needs.

公开接口的线性业务流程评估

Projection -> pool construction -> scoring -> causal selection -> token expansion remains linear. Fallbacks and public backend names remain available. FP8 compression calls retain their old call form for compatibility with older LMDeploy versions.

单测建议

Keep the new real-shape and boundary regressions. The paired replay is the acceptance surface for the local numerical target; global KL, multi-update training, cache lifecycle and throughput remain separate checks.

其他 issues

No additional blocking finding. The remaining FP32 scorer reduction-order error is explicitly reported; this change does not claim bitwise score equality or whole-model KL <= 2e-4. Non-SM90 / optional-dependency fallback paths retain legacy arithmetic and are outside this parity claim.

This branch has not been deployed

No deployments
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.

3 participants