状态(Status): Draft 作者(Authors): @husichao 创建日期(Created): 2026-09-04 更新日期(Updated): 2026-09-04 相关 Issue/PR: 待关联
继续使用现有 dsa,在 token index key 之后增加可选 KPool 分支:
dsa
-1
对 GLM-5.3-Flash,原始索引宽度最大为 2048 + 3 = 2051。Ascend 训练路径补齐到 CANN 前向和反向均支持的 K=3072:
2048 + 3 = 2051
K=3072
512 pool × 4 token + 最多 3 个尾部 token = 最多 2051 个有效槽位 2051 个槽位 + 1021 个 -1 padding = SFA K=3072
本文将 query 所在未满 4-token 分组中的已可见 token 称为“尾部 token(tail)”。Padding 只扩展 sparse_indices 的最后一维,原始序列及 K/V Tensor 保持不变。
sparse_indices
本方案覆盖以下生产场景:
约束是保持现有 token DSA 行为兼容,并在 Ascend 训练路径使用 CANN 前向、反向均支持的 SFA 规格。
DSAIndexer
灰色是原 DSA,绿色是 KPool 新增流程,橙色是需要适配的现有模块。原 token DSA 沿用 2048 个 token id 的现有路径。
增加最小配置:
dsa_indexer_candidate_mode: str = "token" # token | kpool dsa_indexer_pool_size: int = 1 # GLM-5.3-Flash 为 4 dsa_indexer_pool_always_select_tail: bool = True
校验规则:
candidate_mode=kpool
pool_size=4
index_topk % pool_size == 0
2048 / 4 = 512
SFA 的 K=3072 是由 index_topk 和 pool_size 推导出的算子规格,不对用户暴露为算法配置。
index_topk
pool_size
DSAIndexer 原有 query、key、weight 投影和打分公式不变。KPool 只压缩 token_k,新增参数 与 GLM-5.3-Flash checkpoint 对齐:
token_k
index_kpool_compress_gate: [index_dim, hidden_size] index_kpool_compress_ape: [pool_size, index_dim]
BSND 直接按 batch 中每条样本的有效长度分 pool;TND 先按 actual_seq_klen 的累计边界 拆出 document,再使用 document-local 位置分 pool。两种布局仅在序列边界和 token id 映射上 不同,后续的 pool 压缩、TopK、展开、tail 追加和补齐流程相同。
actual_seq_klen
简化伪代码:
def kpool_indices(q_index, token_k, weight, hidden_states, seq_meta): POOL_SIZE = 4 POOL_TOPK = 2048 // POOL_SIZE # 512 RAW_WIDTH = 2048 + POOL_SIZE - 1 # 2051 SFA_WIDTH = 3072 # CANN 训练支持的下一档 K pool_keys = [] pool_token_ids = [] # BSND 按 batch 划定边界;TND 按 actual_seq_klen 的累计边界划定 document。 # pool 不能跨样本或 packed document。 for doc in split_by_sequence(hidden_states, token_k, seq_meta): complete_pools = doc.length // POOL_SIZE width = complete_pools * POOL_SIZE keys4 = doc.token_k[:width].reshape( complete_pools, POOL_SIZE, index_dim ) gates4 = linear( doc.hidden[:width], index_kpool_compress_gate ).reshape(complete_pools, POOL_SIZE, index_dim) # pool 内位置维做 FP32 softmax。 alpha = softmax( gates4.float() + index_kpool_compress_ape[None].float(), dim=1, ) pool_keys.append(sum(alpha.to(keys4.dtype) * keys4, dim=1)) pool_token_ids.append( arange(width).reshape(complete_pools, POOL_SIZE) ) pool_k, pool_meta = pack_pool_keys(pool_keys, seq_meta) # cmp_ratio/cmp_residual_k 使算子按每个 query 的局部位置计算可见完整 pool。 # 可见 pool 不足 512 时,未使用槽位返回 -1。 selected_pool_ids, selected_pool_scores = lightning_indexer_v2( q=q_index, k=pool_k, w=weight, topk=POOL_TOPK, q_seq_meta=seq_meta, k_seq_meta=pool_meta, mask_mode=CAUSAL, cmp_ratio=POOL_SIZE, cmp_residual_k=sequence_lengths(seq_meta) % POOL_SIZE, ) # 每个有效 pool id 展开成 4 个原始 token id。 selected_token_ids = expand_pool_ids( selected_pool_ids, pool_token_ids, invalid_value=-1 ) # 固定 2048 槽位 # 每个 query 单独追加当前未满分组中的可见 token,最多 3 个。 q_pos = document_local_query_positions(seq_meta) tail_count = (q_pos + 1) % POOL_SIZE tail_start = q_pos + 1 - tail_count offsets = arange(POOL_SIZE - 1) tail_ids = where( offsets < tail_count[..., None], tail_start[..., None] + offsets, -1, ) # 固定 3 槽位 raw_indices = concat([selected_token_ids, tail_ids], dim=-1) assert raw_indices.shape[-1] == RAW_WIDTH # CANN 要求每行有效值在前、无效值在后。 # 排序不是 Attention 语义要求,但便于确定性和算子校验。 compact_indices = compact_valid_prefix(raw_indices, invalid_value=-1) compact_indices = sort_valid_prefix(compact_indices, invalid_value=-1) sfa_indices = pad(compact_indices, width=SFA_WIDTH, value=-1) # pool id/score 留给 Indexer Loss;3072 token id 只交给 SFA。 return sfa_indices, selected_pool_ids, selected_pool_scores, pool_meta
以第 2050 个 token(零基位置 2049)为例:该 query 可见 512 个完整 pool,当前未满分组包含 2 个尾部 token。该行最多包含 2048 个完整-pool token、2 个尾部 token 和 1022 个 -1, 最终宽度为 3072。
Warning
生产风险:当前缺少两个训练可用的融合算子。
一是带反向的 KPool Compress,需要完成完整 Pool 输入上的 gate + APE + softmax + weighted sum;二是索引构造,需要完成 Pool ID 展开、Tail 追加、 有效前缀整理及补齐到 3072。若直接使用当前伪代码中的小算子和逐 document 处理,Pynative 下 可能产生大量 Kernel Launch、图切分和 Host 下发,长序列场景存在断流及吞吐下降风险。开源 推理融合算子只有前向且面向 FP8 Cache,不能直接覆盖训练反向。
gate + APE + softmax + weighted sum
NVIDIA NeMo AutoModel 的 KPool 路径先产生 index_topk + index_kpool - 1 个索引槽位, 再将有效索引整理为连续前缀,并补齐到 FlashMLA 要求的 512 对齐宽度。本方案采用相同的 索引组织方式,并按 CANN 反向支持的 K 规格将 2051 补齐到 3072。
index_topk + index_kpool - 1
attention_out, softmax_max, softmax_sum = sparse_flash_attention( q, k, v, sparse_indices=sfa_indices, # [..., 3072],有效前缀 + -1 后缀 sparse_block_size=1, actual_seq_lengths_query=actual_seq_qlen, actual_seq_lengths_kv=actual_seq_kvlen, )
融合 SFA 的输入约定为:
CANN SFA 前向允许 sparse_size > 0,并要求有效索引在前、无效索引在后;当前反向明确支持 K=1024、2048、3072...。当前 MindSpore 公共 ops.sparse_flash_attention 仍把 K 固定为 2048,因此 P2 需要将该接口扩展到 CANN 已支持的 K=3072,或接入能透传 3072 的底层封装。 这属于 MindSpore 接口接入,不要求新增 CANN SFA 算法。
sparse_size > 0
K=1024、2048、3072...
ops.sparse_flash_attention
下表说明选择 K=3072 的依据:
2051 → 3072
block=1
K=2051
K=513
block=4
生产风险:2051 → 3072 会引入明显的 Padding 成本。
每行最多只有 2051 个有效索引,至少 1021 个槽位是 -1,约占 3072 宽度的 33%。相对现有 K=2048,int32 索引张量、读写带宽及相关 workspace 的宽度增加 50%;以 B=1、S=128K 为例,单个完整索引张量会从约 1.0 GiB 增至 1.5 GiB。SFA 仍按 K=3072 的算子规格调度, 不能假设 -1 槽位完全没有计算成本,可能增加前向/反向耗时和峰值显存。最终影响必须通过长 序列端到端 Profiling 确认,不能只依据有效 token 数估算。
K=2048
B=1、S=128K
KPool 沿用普通 DSA 的 KL 监督和两阶段训练,变化是把监督和打分的候选粒度从 token 提升为 完整 pool。KPool Indexer 及其新增的 compress gate、APE 参数加入优化器。
索引展开、Tail 追加和补齐只服务 SFA,不进入 Indexer Loss。两阶段的 Attention 和参数更新为:
主 Attention 产生的注意力概率作为停止梯度的监督目标。每 4 个 token 的目标概率相加得到 pool 目标概率,再与 Indexer 产生的 pool score 分布计算 KL Loss:
target_pool_prob[j] = sum(target_token_prob[4*j : 4*j+4])
TopK 是离散的,两阶段都由 Indexer Loss 更新 Indexer 参数;LM Loss 不经过 TopK 回传。
规则:
因此阶段二虽然不直接对 TopK id 求导,但 Indexer 分数更新后,后续 step 的 pool 排序和选择会变化。
生产风险:当前未找到 GLM-5.3 KPool Indexer Loss 的开源训练实现。
GLM-5.3-Flash 公开代码只提供 KPool 前向和推理路径,没有公开 Pool Loss 公式、Tail 监督规则及 训练融合算子。本方案将 4 个 token 的 Attention 目标概率相加作为 Pool target,并对完整 Pool 重新归一化;Tail 不参与 Pool Loss。这是从原 DSA Token Loss 推导的适配方案,不是 GLM-5.3-Flash 官方实现,因此无法验证其与官方 Loss 定义或训练梯度一致。当前只能验证:融合 实现与本方案非融合参考公式的前向和梯度一致;使用官方 checkpoint 时,Pool Key、Pool Score、 TopK 和 Tail 索引与开源推理实现一致;训练后 Indexer Loss、目标 Attention 召回率及端到端模型 精度满足要求。这些结果只能证明本方案可用,不能证明其复现了 GLM-5.3-Flash 的官方训练行为。 若生产要求严格复现,必须由原厂提供 Loss 定义或训练实现。
BSND 按样本、TND 按 cu_seqlens 中的 document 独立分 pool;pool 和尾部 token 都以 document 为边界生成。
cu_seqlens
mindformers/parallel_core/transformer_config.py
mindformers/parallel_core/transformer_config_utils.py
.../experimental_attention_variant/dsa_kpool.py
.../experimental_attention_variant/dsa_indexer.py
.../experimental_attention_variant/dsa.py
.../experimental_attention_variant/dsa_attention.py
.../experimental_attention_variant/dsa_indexer_loss.py
mindformers/pynative/base_models/gpt/parallelize.py
tests/st/test_ut/test_pynative/test_transformers/test_dsa/
不新增顶层 dsa_kpool 类型;experimental_attention_variant="dsa" 保持不变。
dsa_kpool
experimental_attention_variant="dsa"
cmp_ratio=4
cmp_residual_k
S<4
S%4=1/2/3
关键验收关系:
K=3072 padded SFA ≈ K=2051 对照 ≈ dense masked Attention
生产前需要重点收敛以下风险:
本方案参考 NVIDIA NeMo AutoModel 的 KPool 索引组织与 padding 路径,并结合 MindSpore/CANN SFA 的前向、反向约束适配 Ascend 训练。
index_kpool=4
index_topk=2048
状态(Status): Draft
作者(Authors): @husichao
创建日期(Created): 2026-09-04
更新日期(Updated): 2026-09-04
相关 Issue/PR: 待关联
1. 概述
1.1 简介与目标
继续使用现有
dsa,在 token index key 之后增加可选 KPool 分支:-1补到融合算子支持的宽度;对 GLM-5.3-Flash,原始索引宽度最大为
2048 + 3 = 2051。Ascend 训练路径补齐到CANN 前向和反向均支持的
K=3072:本文将 query 所在未满 4-token 分组中的已可见 token 称为“尾部 token(tail)”。Padding 只扩展
sparse_indices的最后一维,原始序列及 K/V Tensor 保持不变。2. 用例分析
本方案覆盖以下生产场景:
约束是保持现有 token DSA 行为兼容,并在 Ascend 训练路径使用 CANN 前向、反向均支持的 SFA 规格。
3. 方案设计
3.1 总体流程
DSAIndexer灰色是原 DSA,绿色是 KPool 新增流程,橙色是需要适配的现有模块。原 token DSA 沿用
2048 个 token id 的现有路径。
3.2 配置
增加最小配置:
dsa_indexer_candidate_mode: str = "token" # token | kpool dsa_indexer_pool_size: int = 1 # GLM-5.3-Flash 为 4 dsa_indexer_pool_always_select_tail: bool = True校验规则:
candidate_mode=kpool时,第一版只接受pool_size=4;index_topk % pool_size == 0,GLM-5.3-Flash 为2048 / 4 = 512个 pool;SFA 的
K=3072是由index_topk和pool_size推导出的算子规格,不对用户暴露为算法配置。3.3 KPool 与索引构造
DSAIndexer原有 query、key、weight 投影和打分公式不变。KPool 只压缩token_k,新增参数与 GLM-5.3-Flash checkpoint 对齐:
BSND 直接按 batch 中每条样本的有效长度分 pool;TND 先按
actual_seq_klen的累计边界拆出 document,再使用 document-local 位置分 pool。两种布局仅在序列边界和 token id 映射上
不同,后续的 pool 压缩、TopK、展开、tail 追加和补齐流程相同。
简化伪代码:
def kpool_indices(q_index, token_k, weight, hidden_states, seq_meta): POOL_SIZE = 4 POOL_TOPK = 2048 // POOL_SIZE # 512 RAW_WIDTH = 2048 + POOL_SIZE - 1 # 2051 SFA_WIDTH = 3072 # CANN 训练支持的下一档 K pool_keys = [] pool_token_ids = [] # BSND 按 batch 划定边界;TND 按 actual_seq_klen 的累计边界划定 document。 # pool 不能跨样本或 packed document。 for doc in split_by_sequence(hidden_states, token_k, seq_meta): complete_pools = doc.length // POOL_SIZE width = complete_pools * POOL_SIZE keys4 = doc.token_k[:width].reshape( complete_pools, POOL_SIZE, index_dim ) gates4 = linear( doc.hidden[:width], index_kpool_compress_gate ).reshape(complete_pools, POOL_SIZE, index_dim) # pool 内位置维做 FP32 softmax。 alpha = softmax( gates4.float() + index_kpool_compress_ape[None].float(), dim=1, ) pool_keys.append(sum(alpha.to(keys4.dtype) * keys4, dim=1)) pool_token_ids.append( arange(width).reshape(complete_pools, POOL_SIZE) ) pool_k, pool_meta = pack_pool_keys(pool_keys, seq_meta) # cmp_ratio/cmp_residual_k 使算子按每个 query 的局部位置计算可见完整 pool。 # 可见 pool 不足 512 时,未使用槽位返回 -1。 selected_pool_ids, selected_pool_scores = lightning_indexer_v2( q=q_index, k=pool_k, w=weight, topk=POOL_TOPK, q_seq_meta=seq_meta, k_seq_meta=pool_meta, mask_mode=CAUSAL, cmp_ratio=POOL_SIZE, cmp_residual_k=sequence_lengths(seq_meta) % POOL_SIZE, ) # 每个有效 pool id 展开成 4 个原始 token id。 selected_token_ids = expand_pool_ids( selected_pool_ids, pool_token_ids, invalid_value=-1 ) # 固定 2048 槽位 # 每个 query 单独追加当前未满分组中的可见 token,最多 3 个。 q_pos = document_local_query_positions(seq_meta) tail_count = (q_pos + 1) % POOL_SIZE tail_start = q_pos + 1 - tail_count offsets = arange(POOL_SIZE - 1) tail_ids = where( offsets < tail_count[..., None], tail_start[..., None] + offsets, -1, ) # 固定 3 槽位 raw_indices = concat([selected_token_ids, tail_ids], dim=-1) assert raw_indices.shape[-1] == RAW_WIDTH # CANN 要求每行有效值在前、无效值在后。 # 排序不是 Attention 语义要求,但便于确定性和算子校验。 compact_indices = compact_valid_prefix(raw_indices, invalid_value=-1) compact_indices = sort_valid_prefix(compact_indices, invalid_value=-1) sfa_indices = pad(compact_indices, width=SFA_WIDTH, value=-1) # pool id/score 留给 Indexer Loss;3072 token id 只交给 SFA。 return sfa_indices, selected_pool_ids, selected_pool_scores, pool_meta以第 2050 个 token(零基位置 2049)为例:该 query 可见 512 个完整 pool,当前未满分组包含
2 个尾部 token。该行最多包含 2048 个完整-pool token、2 个尾部 token 和 1022 个
-1,最终宽度为 3072。
Warning
生产风险:当前缺少两个训练可用的融合算子。
一是带反向的 KPool Compress,需要完成完整 Pool 输入上的
gate + APE + softmax + weighted sum;二是索引构造,需要完成 Pool ID 展开、Tail 追加、有效前缀整理及补齐到 3072。若直接使用当前伪代码中的小算子和逐 document 处理,Pynative 下
可能产生大量 Kernel Launch、图切分和 Host 下发,长序列场景存在断流及吞吐下降风险。开源
推理融合算子只有前向且面向 FP8 Cache,不能直接覆盖训练反向。
3.4 一次调用融合 SFA
NVIDIA NeMo AutoModel 的 KPool 路径先产生
index_topk + index_kpool - 1个索引槽位,再将有效索引整理为连续前缀,并补齐到 FlashMLA 要求的 512 对齐宽度。本方案采用相同的
索引组织方式,并按 CANN 反向支持的 K 规格将 2051 补齐到 3072。
attention_out, softmax_max, softmax_sum = sparse_flash_attention( q, k, v, sparse_indices=sfa_indices, # [..., 3072],有效前缀 + -1 后缀 sparse_block_size=1, actual_seq_lengths_query=actual_seq_qlen, actual_seq_lengths_kv=actual_seq_kvlen, )融合 SFA 的输入约定为:
-1后缀;-1padding。CANN SFA 前向允许
sparse_size > 0,并要求有效索引在前、无效索引在后;当前反向明确支持K=1024、2048、3072...。当前 MindSpore 公共ops.sparse_flash_attention仍把 K 固定为2048,因此 P2 需要将该接口扩展到 CANN 已支持的
K=3072,或接入能透传 3072 的底层封装。这属于 MindSpore 接口接入,不要求新增 CANN SFA 算法。
下表说明选择 K=3072 的依据:
2051 → 3072,block=1K=2051,block=1K=513,block=4Warning
生产风险:
2051 → 3072会引入明显的 Padding 成本。每行最多只有 2051 个有效索引,至少 1021 个槽位是
-1,约占 3072 宽度的 33%。相对现有K=2048,int32 索引张量、读写带宽及相关 workspace 的宽度增加 50%;以B=1、S=128K为例,单个完整索引张量会从约 1.0 GiB 增至 1.5 GiB。SFA 仍按
K=3072的算子规格调度,不能假设
-1槽位完全没有计算成本,可能增加前向/反向耗时和峰值显存。最终影响必须通过长序列端到端 Profiling 确认,不能只依据有效 token 数估算。
3.5 Indexer Loss
KPool 沿用普通 DSA 的 KL 监督和两阶段训练,变化是把监督和打分的候选粒度从 token 提升为
完整 pool。KPool Indexer 及其新增的 compress gate、APE 参数加入优化器。
索引展开、Tail 追加和补齐只服务 SFA,不进入 Indexer Loss。两阶段的 Attention 和参数更新为:
主 Attention 产生的注意力概率作为停止梯度的监督目标。每 4 个 token 的目标概率相加得到
pool 目标概率,再与 Indexer 产生的 pool score 分布计算 KL Loss:
TopK 是离散的,两阶段都由 Indexer Loss 更新 Indexer 参数;LM Loss 不经过 TopK 回传。
规则:
-1槽位只服务 Attention 算子对齐,不进入 Indexer Loss。因此阶段二虽然不直接对 TopK id 求导,但 Indexer 分数更新后,后续 step 的 pool 排序和选择会变化。
Warning
生产风险:当前未找到 GLM-5.3 KPool Indexer Loss 的开源训练实现。
GLM-5.3-Flash 公开代码只提供 KPool 前向和推理路径,没有公开 Pool Loss 公式、Tail 监督规则及
训练融合算子。本方案将 4 个 token 的 Attention 目标概率相加作为 Pool target,并对完整 Pool
重新归一化;Tail 不参与 Pool Loss。这是从原 DSA Token Loss 推导的适配方案,不是
GLM-5.3-Flash 官方实现,因此无法验证其与官方 Loss 定义或训练梯度一致。当前只能验证:融合
实现与本方案非融合参考公式的前向和梯度一致;使用官方 checkpoint 时,Pool Key、Pool Score、
TopK 和 Tail 索引与开源推理实现一致;训练后 Indexer Loss、目标 Attention 召回率及端到端模型
精度满足要求。这些结果只能证明本方案可用,不能证明其复现了 GLM-5.3-Flash 的官方训练行为。
若生产要求严格复现,必须由原厂提供 Loss 定义或训练实现。
3.6 并行与 checkpoint
避免 pool 跨 CP 边界断开。新增的是 pool 边界处理,不引入新的并行语义。
BSND 按样本、TND 按
cu_seqlens中的 document 独立分 pool;pool 和尾部 token 都以document 为边界生成。
3.7 代码改动
mindformers/parallel_core/transformer_config.pymindformers/parallel_core/transformer_config_utils.py.../experimental_attention_variant/dsa_kpool.py.../experimental_attention_variant/dsa_indexer.py.../experimental_attention_variant/dsa.py.../experimental_attention_variant/dsa_attention.py.../experimental_attention_variant/dsa_indexer_loss.pymindformers/pynative/base_models/gpt/parallelize.pytests/st/test_ut/test_pynative/test_transformers/test_dsa/不新增顶层
dsa_kpool类型;experimental_attention_variant="dsa"保持不变。3.8 分阶段交付
P1:单卡算法正确性
P2:融合算子与训练闭环
cmp_ratio=4和cmp_residual_k;K=3072;P3:生产并行与工程化
4. 测试设计
S<4、S%4=1/2/3、可见 pool 少于 512、空 pool-1padding、K=3072关键验收关系:
5. 缺点与风险
生产前需要重点收敛以下风险:
2051 → 3072至少引入 1021 个-1槽位,索引宽度相对 K=2048 增加 50%,需实测 SFA 耗时和峰值显存;6. 现有技术
本方案参考 NVIDIA NeMo AutoModel 的 KPool 索引组织与 padding 路径,并结合 MindSpore/CANN SFA 的前向、反向约束适配 Ascend 训练。
参考资料
index_kpool=4、index_topk=20487. 未解决问题
K=3072padding 在目标长序列、并行配置上的吞吐与峰值显存是否满足生产门槛。