Merged
feat(moe): migrate gate operators #9
wangliweiAdd on Sep 17
feat(moe): migrate gate operators #9
Merged
Total: 16 files changed+2129-4
| @@ -4,7 +4,7 @@ | |||
| 4 | 4 | ||
| 5 | > **硬件支持范围**:本代码仓仅支持 Ascend A5 及以上架构,不保证在更早的 Ascend 架构上可编译或正确运行。 | 5 | > **硬件支持范围**:本代码仓仅支持 Ascend A5 及以上架构,不保证在更早的 Ascend 架构上可编译或正确运行。 |
| 6 | 6 | ||
| 7 | -当前仓库提供基础工程框架:Engram 算子族包含 `engram_fused_weight`、`engram_gate_fwd`、`engram_gate_bwd`、`engram_grad_w_reduce` 和 `engram_sinkhorn_step`,覆盖前向、反向、分片梯度归并及 Sinkhorn 行归一化。所有算子均接入分级参数生成、确定性随机种子、NPU 显存画像、A5 性能回归框架、正则筛选、xdist 多卡绑定以及分阶段测试调度。 | 7 | +当前仓库提供基础工程框架:Engram 算子族覆盖前向、反向、分片梯度归并及 Sinkhorn 行归一化;MoE 算子族提供 `topk_gate`、`moe_topk_gate` 和 `normalize_weight`。算子测试接入分级参数生成、确定性随机种子、NPU 显存画像、A5 性能回归框架、正则筛选、xdist 多卡绑定以及分阶段测试调度。 |
| 8 | 8 | ||
| 9 | ## 设计原则 | 9 | ## 设计原则 |
| 10 | 10 | ||
| @@ -21,11 +21,13 @@ ops-tilelang/ | |||
| 21 | ├── src/cann_ops_tilelang/ | 21 | ├── src/cann_ops_tilelang/ |
| 22 | │ ├── config.py # Ascend 环境与运行配置 | 22 | │ ├── config.py # Ascend 环境与运行配置 |
| 23 | │ ├── utils.py # 通用纯 Python 工具 | 23 | │ ├── utils.py # 通用纯 Python 工具 |
| 24 | -│ └── engram/ # Engram 前向、反向和梯度归并算子 | 24 | +│ ├── engram/ # Engram 前向、反向和梯度归并算子 |
| 25 | +│ └── moe/ # MoE 路由和权重归一化算子 | ||
| 25 | ├── tests/ | 26 | ├── tests/ |
| 26 | │ ├── testing/ # 参数生成、数值断言和 benchmark 工具 | 27 | │ ├── testing/ # 参数生成、数值断言和 benchmark 工具 |
| 27 | │ ├── pytest_*_plugin.py # benchmark/显存/seed/xdist/warning 插件 | 28 | │ ├── pytest_*_plugin.py # benchmark/显存/seed/xdist/warning 插件 |
| 28 | -│ └── engram/ # 算子测试、PyTorch reference、benchmark 与显存画像 | 29 | +│ ├── engram/ # Engram 测试与 PyTorch reference |
| 30 | +│ └── moe/ # MoE 测试与 PyTorch reference | ||
| 29 | ├── docs/ # 架构和新增算子指南 | 31 | ├── docs/ # 架构和新增算子指南 |
| 30 | ├── scripts/ # 本地/CI 执行入口 | 32 | ├── scripts/ # 本地/CI 执行入口 |
| 31 | └── pyproject.toml # 构建、依赖和工具配置 | 33 | └── pyproject.toml # 构建、依赖和工具配置 |
| @@ -115,6 +117,24 @@ row_norm, col_sumsq = engram_sinkhorn_step(matrix, row_scale, col_scale, eps=1e- | |||
| 115 | 117 | ||
| 116 | 三个输入必须位于同一 NPU、连续且为 FP32,并且不能共享存储。`matrix` 的形状为 `(num_rows, head_dim)`,`head_dim` 必须是正的 256 倍数;调用方需保证 `row_scale` 包含非负的 Sinkhorn 缩放系数。已验证的典型维度为 256 和 5120。由于 `SimtVF` 需预留线程上下文,wrapper 会拒绝共享 UB 估算超过 216 KiB 的 shape。`num_rows=0` 时返回空的 `row_norm` 和全零的 `col_sumsq`。 | 118 | 三个输入必须位于同一 NPU、连续且为 FP32,并且不能共享存储。`matrix` 的形状为 `(num_rows, head_dim)`,`head_dim` 必须是正的 256 倍数;调用方需保证 `row_scale` 包含非负的 Sinkhorn 缩放系数。已验证的典型维度为 256 和 5120。由于 `SimtVF` 需预留线程上下文,wrapper 会拒绝共享 UB 估算超过 216 KiB 的 shape。`num_rows=0` 时返回空的 `row_norm` 和全零的 `col_sumsq`。 |
| 117 | 119 | ||
| 120 | +MoE 路由算子接收连续的 NPU FP32 分数。`topk_gate` 稳定选择每个 token 的专家索引,分数相同时选择较小索引;`moe_topk_gate` 融合打分、偏置、掩码、固定路由、随机路由和逻辑到物理专家映射;`normalize_weight` 返回每行分母及归一化权重: | ||
| 121 | + | ||
| 122 | +```python | ||
| 123 | +from cann_ops_tilelang import moe_topk_gate, normalize_weight, topk_gate | ||
| 124 | + | ||
| 125 | +topk_idx = topk_gate(scores, num_topk=6) | ||
| 126 | +denominator, normalized_weights = normalize_weight(topk_weights) | ||
| 127 | +topk_idx, topk_weights = moe_topk_gate( | ||
| 128 | + logits, | ||
| 129 | + num_topk=6, | ||
| 130 | + use_shared_as_routed=False, | ||
| 131 | + num_shared_experts=1, | ||
| 132 | + routed_scaling_factor=1.0, | ||
| 133 | + ep_rank=0, | ||
| 134 | + scoring_func='sigmoid', | ||
| 135 | +) | ||
| 136 | +``` | ||
| 137 | + | ||
| 118 | ## 测试 | 138 | ## 测试 |
| 119 | 139 | ||
| 120 | ```bash | 140 | ```bash |
| @@ -135,6 +155,11 @@ pytest tests/engram/test_engram_fused_weight.py -m 'not benchmark' --refresh-mem | |||
| 135 | pytest tests/engram -m benchmark --run-benchmark --refresh-baseline | 155 | pytest tests/engram -m benchmark --run-benchmark --refresh-baseline |
| 136 | pytest tests/engram -m 'not benchmark' --refresh-mem | 156 | pytest tests/engram -m 'not benchmark' --refresh-mem |
| 137 | 157 | ||
| 158 | +# 运行或刷新 MoE 算子族 | ||
这里只是举例,不是所有的算子都在这里列一下,前面已经有Engram举例了,所以这里的moe就不用加了。 ![]() ![]() | |||
| 159 | +pytest tests/moe -m 'npu and not benchmark' | ||
| 160 | +pytest tests/moe -m benchmark --run-benchmark --refresh-baseline | ||
| 161 | +pytest tests/moe -m 'not benchmark' --refresh-mem | ||
| 162 | + | ||
| 138 | # 标准入口 | 163 | # 标准入口 |
| 139 | ./scripts/run_test.sh | 164 | ./scripts/run_test.sh |
| 140 | ``` | 165 | ``` |
| @@ -97,7 +97,7 @@ engram_fused_weight(weight_hidden, weight_embed) | |||
| 97 | ## 扩展方向 | 97 | ## 扩展方向 |
| 98 | 98 | ||
| 99 | - `src/cann_ops_tilelang/quant/`:公共 scaling-factor layout 和 FP8/FP4 类型系统; | 99 | - `src/cann_ops_tilelang/quant/`:公共 scaling-factor layout 和 FP8/FP4 类型系统; |
| 100 | -- `src/cann_ops_tilelang/moe/`:TopK、routing、expert mapping; | 100 | +- `src/cann_ops_tilelang/moe/`:在现有 TopK、routing 和 expert mapping 基础上扩展 expert dispatch/combine; |
| 101 | - `src/cann_ops_tilelang/norm/`:RMSNorm、LayerNorm 和融合残差; | 101 | - `src/cann_ops_tilelang/norm/`:RMSNorm、LayerNorm 和融合残差; |
| 102 | - `src/cann_ops_tilelang/attention/`:RoPE、KV Cache、prefill/decode; | 102 | - `src/cann_ops_tilelang/attention/`:RoPE、KV Cache、prefill/decode; |
| 103 | - `tests/<operator>/benchmark/`:在算子测试目录内按测试文件存储 JSONL 性能基线。 | 103 | - `tests/<operator>/benchmark/`:在算子测试目录内按测试文件存储 JSONL 性能基线。 |
| @@ -6,6 +6,10 @@ __all__ = [ | |||
| 6 | 'engram_gate_fwd', | 6 | 'engram_gate_fwd', |
| 7 | 'engram_grad_w_reduce', | 7 | 'engram_grad_w_reduce', |
| 8 | 'engram_sinkhorn_step', | 8 | 'engram_sinkhorn_step', |
| 9 | + 'moe_topk_gate', | ||
| 10 | + 'normalize_weight', | ||
| 11 | + 'topk_gate', | ||
| 9 | ] | 12 | ] |
| 10 | 13 | ||
| 11 | from .engram import engram_fused_weight, engram_gate_bwd, engram_gate_fwd, engram_grad_w_reduce, engram_sinkhorn_step | 14 | from .engram import engram_fused_weight, engram_gate_bwd, engram_gate_fwd, engram_grad_w_reduce, engram_sinkhorn_step |
| 15 | +from .moe import moe_topk_gate, normalize_weight, topk_gate | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | +"""Mixture-of-Experts operators.""" | ||
| 2 | + | ||
| 3 | +__all__ = ['moe_topk_gate', 'normalize_weight', 'topk_gate'] | ||
| 4 | + | ||
| 5 | +from .moe_topk_gate import moe_topk_gate | ||
| 6 | +from .normalize_weight import normalize_weight | ||
| 7 | +from .topk_gate import topk_gate | ||
| @@ -0,0 +1,239 @@ | |||
| 1 | +"""Public interface for fused Mixture-of-Experts routing.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import math | ||
| 6 | + | ||
| 7 | +import torch | ||
| 8 | + | ||
| 9 | +from cann_ops_tilelang.config import require_ascend, should_print_kernel_source | ||
| 10 | + | ||
| 11 | +from .moe_topk_gate_kernel import get_moe_topk_gate_kernel | ||
| 12 | +from .scoring import ScoringFunc | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +def _check_npu_tensor( | ||
| 16 | + name: str, | ||
| 17 | + tensor: torch.Tensor, | ||
| 18 | + *, | ||
| 19 | + dtype: torch.dtype, | ||
| 20 | + device: torch.device, | ||
| 21 | + ndim: int, | ||
| 22 | + contiguous: bool = True, | ||
| 23 | +) -> None: | ||
| 24 | + if not isinstance(tensor, torch.Tensor): | ||
| 25 | + raise TypeError(f'{name} must be a torch.Tensor.') | ||
| 26 | + if tensor.device.type != 'npu': | ||
| 27 | + raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.') | ||
| 28 | + if tensor.device != device: | ||
| 29 | + raise ValueError(f'{name} must be on {device}, got {tensor.device}.') | ||
| 30 | + if tensor.dtype != dtype: | ||
| 31 | + raise TypeError(f'{name} must use {dtype}, got {tensor.dtype}.') | ||
| 32 | + if tensor.ndim != ndim: | ||
| 33 | + raise ValueError(f'{name} must have {ndim} dimensions, got {tensor.ndim}.') | ||
| 34 | + if contiguous and not tensor.is_contiguous(): | ||
| 35 | + raise ValueError(f'{name} must be contiguous.') | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +def _shares_storage(left: torch.Tensor, right: torch.Tensor) -> bool: | ||
| 39 | + return ( | ||
| 40 | + left.numel() > 0 | ||
| 41 | + and right.numel() > 0 | ||
| 42 | + and left.untyped_storage().data_ptr() == right.untyped_storage().data_ptr() | ||
| 43 | + ) | ||
| 44 | + | ||
| 45 | + | ||
| 46 | +def moe_topk_gate( | ||
| 47 | + logits: torch.Tensor, | ||
| 48 | + num_topk: int, | ||
| 49 | + use_shared_as_routed: bool, | ||
| 50 | + num_shared_experts: int, | ||
| 51 | + routed_scaling_factor: float, | ||
| 52 | + ep_rank: int, | ||
| 53 | + scoring_func: str = 'identity', | ||
| 54 | + mask: torch.Tensor | None = None, | ||
| 55 | + bias: torch.Tensor | None = None, | ||
| 56 | + image_bias: torch.Tensor | None = None, | ||
| 57 | + image_token_mask: torch.Tensor | None = None, | ||
| 58 | + fix_routing_mask: torch.Tensor | None = None, | ||
| 59 | + to_physical_map: torch.Tensor | None = None, | ||
| 60 | + logical_count: torch.Tensor | None = None, | ||
| 61 | + unmapped_topk_idx: torch.Tensor | None = None, | ||
| 62 | + force_random: torch.Tensor | None = None, | ||
| 63 | + out: tuple[torch.Tensor, torch.Tensor] | None = None, | ||
| 64 | +) -> tuple[torch.Tensor, torch.Tensor]: | ||
| 65 | + """Compute stable top-k expert routing on Ascend.""" | ||
| 66 | + | ||
| 67 | + if not isinstance(logits, torch.Tensor): | ||
| 68 | + raise TypeError('logits must be a torch.Tensor.') | ||
| 69 | + if not isinstance(num_topk, int) or isinstance(num_topk, bool): | ||
| 70 | + raise TypeError(f'num_topk must be an int, got {type(num_topk).__name__}.') | ||
| 71 | + if not isinstance(use_shared_as_routed, bool): | ||
| 72 | + raise TypeError('use_shared_as_routed must be a bool.') | ||
| 73 | + if not isinstance(num_shared_experts, int) or isinstance(num_shared_experts, bool): | ||
| 74 | + raise TypeError(f'num_shared_experts must be an int, got {type(num_shared_experts).__name__}.') | ||
| 75 | + if not isinstance(routed_scaling_factor, (int, float)) or isinstance(routed_scaling_factor, bool): | ||
| 76 | + raise TypeError('routed_scaling_factor must be a real number.') | ||
| 77 | + if not math.isfinite(routed_scaling_factor): | ||
| 78 | + raise ValueError('routed_scaling_factor must be finite.') | ||
| 79 | + if not isinstance(ep_rank, int) or isinstance(ep_rank, bool): | ||
| 80 | + raise TypeError(f'ep_rank must be an int, got {type(ep_rank).__name__}.') | ||
| 81 | + if ep_rank < 0: | ||
| 82 | + raise ValueError(f'ep_rank must be non-negative, got {ep_rank}.') | ||
| 83 | + if not isinstance(scoring_func, str): | ||
| 84 | + raise TypeError('scoring_func must be a string.') | ||
| 85 | + | ||
| 86 | + require_ascend() | ||
| 87 | + device = logits.device | ||
| 88 | + _check_npu_tensor('logits', logits, dtype=torch.float32, device=device, ndim=2) | ||
| 89 | + num_tokens, num_routed_experts = logits.shape | ||
| 90 | + if num_topk <= 0 or num_topk > min(32, num_routed_experts): | ||
| 91 | + raise ValueError(f'num_topk must be in [1, min(32, {num_routed_experts})], got {num_topk}.') | ||
| 92 | + | ||
| 93 | + if use_shared_as_routed: | ||
| 94 | + if num_shared_experts not in (1, 2): | ||
| 95 | + raise ValueError(f'num_shared_experts must be 1 or 2, got {num_shared_experts}.') | ||
| 96 | + if num_topk % num_shared_experts != 0: | ||
| 97 | + raise ValueError('num_topk must be divisible by num_shared_experts.') | ||
| 98 | + routed_group_size = num_topk // num_shared_experts | ||
| 99 | + if num_routed_experts % routed_group_size != 0: | ||
| 100 | + raise ValueError('num_routed_experts must be divisible by num_topk // num_shared_experts.') | ||
| 101 | + else: | ||
| 102 | + num_shared_experts = 0 | ||
| 103 | + | ||
| 104 | + has_bias = bias is not None | ||
| 105 | + if bias is not None: | ||
| 106 | + _check_npu_tensor('bias', bias, dtype=torch.float32, device=device, ndim=1) | ||
| 107 | + if bias.shape != (num_routed_experts,): | ||
| 108 | + raise ValueError(f'bias must have shape {(num_routed_experts,)}, got {tuple(bias.shape)}.') | ||
| 109 | + | ||
| 110 | + has_image_token_mask = image_token_mask is not None | ||
| 111 | + if has_image_token_mask != (image_bias is not None): | ||
| 112 | + raise ValueError('image_bias and image_token_mask must be both provided or both None.') | ||
| 113 | + if image_bias is not None and image_token_mask is not None: | ||
| 114 | + _check_npu_tensor('image_bias', image_bias, dtype=torch.float32, device=device, ndim=1) | ||
| 115 | + _check_npu_tensor('image_token_mask', image_token_mask, dtype=torch.bool, device=device, ndim=1) | ||
| 116 | + if image_bias.shape != (num_routed_experts,): | ||
| 117 | + raise ValueError(f'image_bias must have shape {(num_routed_experts,)}, got {tuple(image_bias.shape)}.') | ||
| 118 | + if image_token_mask.shape != (num_tokens,): | ||
| 119 | + raise ValueError(f'image_token_mask must have shape {(num_tokens,)}, got {tuple(image_token_mask.shape)}.') | ||
| 120 | + | ||
| 121 | + if mask is not None: | ||
| 122 | + _check_npu_tensor('mask', mask, dtype=torch.bool, device=device, ndim=1) | ||
| 123 | + if mask.shape != (num_tokens,): | ||
| 124 | + raise ValueError(f'mask must have shape {(num_tokens,)}, got {tuple(mask.shape)}.') | ||
| 125 | + if force_random is not None: | ||
| 126 | + _check_npu_tensor('force_random', force_random, dtype=torch.bool, device=device, ndim=1) | ||
| 127 | + if force_random.shape != (num_tokens,): | ||
| 128 | + raise ValueError(f'force_random must have shape {(num_tokens,)}, got {tuple(force_random.shape)}.') | ||
| 129 | + | ||
| 130 | + if (to_physical_map is None) != (logical_count is None): | ||
| 131 | + raise ValueError('to_physical_map and logical_count must be both provided or both None.') | ||
| 132 | + num_logical_experts = num_routed_experts + num_shared_experts | ||
| 133 | + if to_physical_map is not None and logical_count is not None: | ||
| 134 | + _check_npu_tensor('to_physical_map', to_physical_map, dtype=torch.int32, device=device, ndim=2) | ||
| 135 | + _check_npu_tensor('logical_count', logical_count, dtype=torch.int32, device=device, ndim=1) | ||
| 136 | + if to_physical_map.shape[0] != num_logical_experts or to_physical_map.shape[1] <= 0: | ||
| 137 | + raise ValueError( | ||
| 138 | + 'to_physical_map must have shape ' | ||
| 139 | + f'({num_logical_experts}, num_duplicate_experts), got {tuple(to_physical_map.shape)}.' | ||
| 140 | + ) | ||
| 141 | + if logical_count.shape != (num_logical_experts,): | ||
| 142 | + raise ValueError( | ||
| 143 | + f'logical_count must have shape {(num_logical_experts,)}, got {tuple(logical_count.shape)}.' | ||
| 144 | + ) | ||
| 145 | + | ||
| 146 | + if unmapped_topk_idx is not None: | ||
| 147 | + _check_npu_tensor( | ||
| 148 | + 'unmapped_topk_idx', | ||
| 149 | + unmapped_topk_idx, | ||
| 150 | + dtype=torch.int64, | ||
| 151 | + device=device, | ||
| 152 | + ndim=2, | ||
| 153 | + contiguous=False, | ||
| 154 | + ) | ||
| 155 | + if unmapped_topk_idx.shape != (num_tokens, num_topk): | ||
| 156 | + raise ValueError( | ||
| 157 | + f'unmapped_topk_idx must have shape {(num_tokens, num_topk)}, got {tuple(unmapped_topk_idx.shape)}.' | ||
| 158 | + ) | ||
| 159 | + if unmapped_topk_idx.stride(1) != 1: | ||
| 160 | + raise ValueError('unmapped_topk_idx must have a last-dimension stride of 1.') | ||
| 161 | + if fix_routing_mask is not None: | ||
| 162 | + if unmapped_topk_idx is None: | ||
| 163 | + raise ValueError('fix_routing_mask requires unmapped_topk_idx.') | ||
| 164 | + _check_npu_tensor('fix_routing_mask', fix_routing_mask, dtype=torch.bool, device=device, ndim=1) | ||
| 165 | + if fix_routing_mask.shape != (num_tokens,): | ||
| 166 | + raise ValueError(f'fix_routing_mask must have shape {(num_tokens,)}, got {tuple(fix_routing_mask.shape)}.') | ||
| 167 | + | ||
| 168 | + scoring_type = ScoringFunc.from_str(scoring_func).value | ||
| 169 | + | ||
| 170 | + output_shape = (num_tokens, num_topk + num_shared_experts) | ||
| 171 | + if out is None: | ||
| 172 | + topk_idx = torch.empty(output_shape, dtype=torch.int64, device=device) | ||
| 173 | + topk_weights = torch.empty(output_shape, dtype=torch.float32, device=device) | ||
| 174 | + else: | ||
| 175 | + if not isinstance(out, tuple) or len(out) != 2: | ||
| 176 | + raise TypeError('out must be a tuple of (topk_idx, topk_weights).') | ||
| 177 | + topk_idx, topk_weights = out | ||
| 178 | + _check_npu_tensor('out[0]', topk_idx, dtype=torch.int64, device=device, ndim=2) | ||
| 179 | + _check_npu_tensor('out[1]', topk_weights, dtype=torch.float32, device=device, ndim=2) | ||
| 180 | + if topk_idx.shape != output_shape: | ||
| 181 | + raise ValueError(f'out[0] must have shape {output_shape}, got {tuple(topk_idx.shape)}.') | ||
| 182 | + if topk_weights.shape != output_shape: | ||
| 183 | + raise ValueError(f'out[1] must have shape {output_shape}, got {tuple(topk_weights.shape)}.') | ||
| 184 | + | ||
| 185 | + if _shares_storage(topk_idx, topk_weights): | ||
| 186 | + raise ValueError('out[0] and out[1] must not share storage.') | ||
| 187 | + inputs = ( | ||
| 188 | + ('logits', logits), | ||
| 189 | + ('mask', mask), | ||
| 190 | + ('bias', bias), | ||
| 191 | + ('image_bias', image_bias), | ||
| 192 | + ('image_token_mask', image_token_mask), | ||
| 193 | + ('fix_routing_mask', fix_routing_mask), | ||
| 194 | + ('to_physical_map', to_physical_map), | ||
| 195 | + ('logical_count', logical_count), | ||
| 196 | + ('unmapped_topk_idx', unmapped_topk_idx), | ||
| 197 | + ('force_random', force_random), | ||
| 198 | + ) | ||
| 199 | + for output_name, output in (('out[0]', topk_idx), ('out[1]', topk_weights)): | ||
| 200 | + for input_name, input_tensor in inputs: | ||
| 201 | + if input_tensor is not None and _shares_storage(output, input_tensor): | ||
| 202 | + raise ValueError(f'{output_name} and {input_name} must not share storage.') | ||
| 203 | + | ||
| 204 | + if num_tokens == 0: | ||
| 205 | + return topk_idx, topk_weights | ||
| 206 | + | ||
| 207 | + kernel = get_moe_topk_gate_kernel( | ||
| 208 | + scoring_type, | ||
| 209 | + num_topk, | ||
| 210 | + num_routed_experts, | ||
| 211 | + mask is not None, | ||
| 212 | + fix_routing_mask is not None, | ||
| 213 | + unmapped_topk_idx is not None, | ||
| 214 | + to_physical_map is not None, | ||
| 215 | + has_bias, | ||
| 216 | + has_image_token_mask, | ||
| 217 | + force_random is not None, | ||
| 218 | + ) | ||
| 219 | + if should_print_kernel_source(): | ||
| 220 | + print(kernel.get_kernel_source()) | ||
| 221 | + | ||
| 222 | + kernel( | ||
| 223 | + logits, | ||
| 224 | + bias, | ||
| 225 | + image_bias, | ||
| 226 | + image_token_mask, | ||
| 227 | + mask, | ||
| 228 | + fix_routing_mask, | ||
| 229 | + to_physical_map, | ||
| 230 | + logical_count, | ||
| 231 | + topk_idx, | ||
| 232 | + unmapped_topk_idx, | ||
| 233 | + topk_weights, | ||
| 234 | + force_random, | ||
| 235 | + num_shared_experts, | ||
| 236 | + float(routed_scaling_factor), | ||
| 237 | + ep_rank, | ||
| 238 | + ) | ||
| 239 | + return topk_idx, topk_weights | ||
| @@ -0,0 +1,631 @@ | |||
| 1 | +import tilelang | ||
| 2 | +from tilelang import language as T | ||
| 3 | +from tilelang.language import simd as S | ||
| 4 | + | ||
| 5 | +from cann_ops_tilelang.config import get_num_vector_cores | ||
| 6 | +from cann_ops_tilelang.utils import align_up, ceil_div | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +def _balanced_vector_reduce(values, count, mask, op): | ||
| 10 | + """Build a balanced SIMD reduction tree for a static vreg count.""" | ||
| 11 | + current = [values[i] for i in range(count)] | ||
| 12 | + while len(current) > 1: | ||
| 13 | + next_level = [op(current[i], current[i + 1], mask) for i in range(0, len(current) - 1, 2)] | ||
| 14 | + if len(current) % 2: | ||
| 15 | + next_level.append(current[-1]) | ||
| 16 | + current = next_level | ||
| 17 | + return current[0] | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +def _balanced_score_index_reduce(scores, indices, count, mask): | ||
| 21 | + """Reduce score/index vreg pairs, preserving the lower index on ties.""" | ||
| 22 | + current = [(scores[i], indices[i]) for i in range(count)] | ||
| 23 | + while len(current) > 1: | ||
| 24 | + next_level = [] | ||
| 25 | + for i in range(0, len(current) - 1, 2): | ||
| 26 | + left_score, left_index = current[i] | ||
| 27 | + right_score, right_index = current[i + 1] | ||
| 28 | + keep_left = S.vcmp(left_score, right_score, mask, 'ge') | ||
| 29 | + next_level.append((S.vmax(left_score, right_score, mask), S.vsel(left_index, right_index, keep_left))) | ||
| 30 | + if len(current) % 2: | ||
| 31 | + next_level.append(current[-1]) | ||
| 32 | + current = next_level | ||
| 33 | + return current[0] | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +@tilelang.jit | ||
| 37 | +def get_moe_topk_gate_kernel( | ||
| 38 | + scoring_type: int, | ||
| 39 | + num_topk: int, | ||
| 40 | + num_routed_experts: int, | ||
| 41 | + mask_exists: bool, | ||
| 42 | + fix_routing_mask_exists: bool, | ||
| 43 | + unmapped_topk_idx_exists: bool, | ||
| 44 | + to_physical_map_exists: bool, | ||
| 45 | + has_bias: bool, | ||
| 46 | + has_image_token_mask: bool, | ||
| 47 | + has_force_random: bool, | ||
| 48 | +): | ||
| 49 | + num_cores = get_num_vector_cores() | ||
| 50 | + vector_length = 64 | ||
| 51 | + num_stages = 2 | ||
| 52 | + # Seven FP32 vectors cover the largest tested expert axis (448) in one | ||
| 53 | + # register-resident TopK scan. Smaller expert counts still allocate only | ||
| 54 | + # ceil_div(num_routed_experts, 64) registers. | ||
| 55 | + max_vregs_per_group = 7 | ||
| 56 | + max_physical_topk = num_topk + 2 | ||
| 57 | + physical_map_threads = 256 | ||
| 58 | + | ||
| 59 | + # Keep the register footprint bounded, and scan additional expert groups | ||
| 60 | + # instead of silently dropping experts beyond the first six vectors. | ||
| 61 | + num_vregs_per_group = ceil_div(min(num_routed_experts, max_vregs_per_group * vector_length), vector_length) | ||
| 62 | + experts_per_group = num_vregs_per_group * vector_length | ||
| 63 | + num_groups = ceil_div(num_routed_experts, experts_per_group) | ||
| 64 | + num_aligned_experts = num_groups * experts_per_group | ||
| 65 | + num_aligned_topk = align_up(max_physical_topk, vector_length) | ||
| 66 | + | ||
| 67 | + num_tokens = T.dynamic('num_tokens') | ||
| 68 | + num_physical_topk = T.dynamic('num_physical_topk') | ||
| 69 | + num_logical_experts = T.dynamic('num_logical_experts') | ||
| 70 | + num_duplicate_experts = T.dynamic('num_duplicate_experts') | ||
| 71 | + unmapped_topk_idx_stride = T.dynamic('unmapped_topk_idx_stride') | ||
| 72 | + | ||
| 73 | + @T.prim_func | ||
| 74 | + def moe_topk_gate_kernel( | ||
| 75 | + logits: T.Tensor[(num_tokens, num_routed_experts), T.float32], | ||
| 76 | + bias: T.Tensor[(num_routed_experts,), T.float32], | ||
| 77 | + image_bias: T.Tensor[(num_routed_experts,), T.float32], | ||
| 78 | + image_token_mask: T.Tensor[(num_tokens,), T.bool], | ||
| 79 | + mask: T.Tensor[(num_tokens,), T.bool], | ||
| 80 | + fix_routing_mask: T.Tensor[(num_tokens,), T.bool], | ||
| 81 | + to_physical_map: T.Tensor[(num_logical_experts, num_duplicate_experts), T.int32], | ||
| 82 | + logical_count: T.Tensor[(num_logical_experts,), T.int32], | ||
| 83 | + topk_idx: T.Tensor[(num_tokens, num_physical_topk), T.int64], | ||
| 84 | + unmapped_topk_idx: T.StridedTensor[(num_tokens, num_topk), (unmapped_topk_idx_stride, 1), T.int64], | ||
| 85 | + topk_weights: T.Tensor[(num_tokens, num_physical_topk), T.float32], | ||
| 86 | + force_random: T.Tensor[(num_tokens,), T.bool], | ||
| 87 | + num_shared_experts: T.int32, | ||
| 88 | + routed_scaling_factor: T.float32, | ||
| 89 | + ep_rank: T.int32, | ||
| 90 | + ): | ||
| 91 | + with T.Kernel(num_cores) as core_id: | ||
| 92 | + logits_ub = T.alloc_shared((num_aligned_experts,), T.float32) | ||
| 93 | + bias_ub = T.alloc_shared((num_aligned_experts,), T.float32) | ||
| 94 | + scores_ub = T.alloc_shared((num_aligned_experts,), T.float32) | ||
| 95 | + ranked_scores_ub = T.alloc_shared((num_aligned_experts,), T.float32) | ||
| 96 | + | ||
| 97 | + # int64 output is written through an interleaved int32 view. The | ||
| 98 | + # even words are the low halves; clearing the buffer sets all high | ||
| 99 | + # halves to zero for non-negative expert indices. | ||
| 100 | + out_idx_ub = T.alloc_shared((num_aligned_topk * 2,), T.int32) | ||
| @@ -0,0 +1,43 @@ | |||
| 1 | +"""Public interface for MoE routing-weight normalization.""" | ||
| 2 | + | ||
| 3 | +import torch | ||
| 4 | + | ||
| 5 | +from cann_ops_tilelang.config import require_ascend, should_print_kernel_source | ||
| 6 | + | ||
| 7 | +from .normalize_weight_kernel import get_normalize_weight_kernel | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +def normalize_weight(topk_weights: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | ||
| 11 | + """Normalize each token's FP32 top-k weights on Ascend. | ||
| 12 | + | ||
| 13 | + ``topk_weights`` must be a contiguous two-dimensional NPU tensor with a | ||
| 14 | + positive top-k dimension. The returned denominator includes the kernel's | ||
| 15 | + ``1e-20`` numerical-stability term. | ||
| 16 | + """ | ||
| 17 | + | ||
| 18 | + if not isinstance(topk_weights, torch.Tensor): | ||
| 19 | + raise TypeError('topk_weights must be a torch.Tensor.') | ||
| 20 | + require_ascend() | ||
| 21 | + if topk_weights.device.type != 'npu': | ||
| 22 | + raise ValueError(f'topk_weights must be an NPU tensor, got {topk_weights.device}.') | ||
| 23 | + if topk_weights.dtype != torch.float32: | ||
| 24 | + raise TypeError(f'topk_weights must use torch.float32, got {topk_weights.dtype}.') | ||
| 25 | + if topk_weights.ndim != 2: | ||
| 26 | + raise ValueError('topk_weights must have shape (num_tokens, num_topk).') | ||
| 27 | + if not topk_weights.is_contiguous(): | ||
| 28 | + raise ValueError('topk_weights must be contiguous.') | ||
| 29 | + | ||
| 30 | + num_tokens, num_topk = topk_weights.shape | ||
| 31 | + if num_topk <= 0: | ||
| 32 | + raise ValueError(f'num_topk must be positive, got {num_topk}.') | ||
| 33 | + | ||
| 34 | + denominator = torch.empty((num_tokens,), dtype=torch.float32, device=topk_weights.device) | ||
| 35 | + normalized_weights = torch.empty((num_tokens, num_topk), dtype=torch.float32, device=topk_weights.device) | ||
| 36 | + if num_tokens == 0: | ||
| 37 | + return denominator, normalized_weights | ||
| 38 | + | ||
| 39 | + kernel = get_normalize_weight_kernel(num_topk=num_topk) | ||
| 40 | + if should_print_kernel_source(): | ||
| 41 | + print(kernel.get_kernel_source()) | ||
| 42 | + kernel(topk_weights, denominator, normalized_weights) | ||
| 43 | + return denominator, normalized_weights | ||
| @@ -0,0 +1,53 @@ | |||
| 1 | +import tilelang | ||
| 2 | +from tilelang import language as T | ||
| 3 | + | ||
| 4 | +from cann_ops_tilelang.config import get_num_vector_cores | ||
| 5 | + | ||
| 6 | + | ||
| 7 | +@tilelang.jit | ||
| 8 | +def get_normalize_weight_kernel(num_topk: int): | ||
| 9 | + """Normalize top-k routing weights on Ascend NPU.""" | ||
| 10 | + num_cores = get_num_vector_cores() | ||
| 11 | + num_tokens = T.dynamic('num_tokens') | ||
| 12 | + | ||
| 13 | + tile = 128 | ||
| 14 | + num_stages = 4 | ||
| 15 | + threads = 128 | ||
| 16 | + | ||
| 17 | + @T.prim_func | ||
| 18 | + def normalize_weight_kernel( | ||
| 19 | + topk_weights: T.Tensor[(num_tokens, num_topk), T.float32], | ||
| 20 | + denominator: T.Tensor[(num_tokens,), T.float32], | ||
| 21 | + normalized_weights: T.Tensor[(num_tokens, num_topk), T.float32], | ||
| 22 | + ): | ||
| 23 | + with T.Kernel(num_cores) as core_id: | ||
| 24 | + for tile_id in T.Persistent( | ||
| 25 | + [T.ceildiv(num_tokens, tile)], num_cores, core_id, group_size=1, num_stages=num_stages | ||
| 26 | + ): | ||
| 27 | + row = tile_id * tile | ||
| 28 | + valid_rows = T.min(tile, num_tokens - row) | ||
| 29 | + weights_ub = T.alloc_shared((tile, num_topk), T.float32) | ||
| 30 | + norm_ub = T.alloc_shared((tile, num_topk), T.float32) | ||
| 31 | + denom_ub = T.alloc_shared((tile,), T.float32) | ||
| 32 | + T.annotate_buffer_versions({weights_ub: num_stages, norm_ub: num_stages, denom_ub: num_stages}) | ||
| 33 | + | ||
| 34 | + T.copy(topk_weights[row : row + valid_rows, :], weights_ub[:valid_rows, :]) | ||
| 35 | + | ||
| 36 | + with T.SimtVF(threads=threads): | ||
| 37 | + weights_frag = T.alloc_fragment((tile, num_topk), T.float32) | ||
| 38 | + denom_frag = T.alloc_fragment((tile,), T.float32) | ||
| 39 | + norm_frag = T.alloc_fragment((tile, num_topk), T.float32) | ||
| 40 | + | ||
| 41 | + T.copy(weights_ub, weights_frag) | ||
| 42 | + T.reduce_sum(weights_frag, denom_frag, dim=1) | ||
| 43 | + for i in T.Parallel(tile): | ||
| 44 | + denom_frag[i] += 1e-20 | ||
| 45 | + for i, j in T.Parallel(tile, num_topk): | ||
| 46 | + norm_frag[i, j] = weights_frag[i, j] / denom_frag[i] | ||
| 47 | + T.copy(denom_frag, denom_ub) | ||
| 48 | + T.copy(norm_frag, norm_ub) | ||
| 49 | + | ||
| 50 | + T.copy(denom_ub[:valid_rows], denominator[row : row + valid_rows]) | ||
| 51 | + T.copy(norm_ub[:valid_rows, :], normalized_weights[row : row + valid_rows, :]) | ||
| 52 | + | ||
| 53 | + return normalize_weight_kernel | ||
| @@ -0,0 +1,18 @@ | |||
| 1 | +from enum import IntEnum | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +class ScoringFunc(IntEnum): | ||
| 5 | + SIGMOID = 0 | ||
| 6 | + SQRTSOFTPLUS = 1 | ||
| 7 | + SOFTMAX = 2 | ||
| 8 | + IDENTITY = 3 | ||
| 9 | + | ||
| 10 | + def __str__(self): | ||
| 11 | + return self.name.lower() | ||
| 12 | + | ||
| 13 | + @classmethod | ||
| 14 | + def from_str(cls, label: str): | ||
| 15 | + try: | ||
| 16 | + return cls[label.upper()] | ||
| 17 | + except (AttributeError, KeyError): | ||
| 18 | + raise ValueError(f'{label} is not a valid {cls.__name__}') from None | ||
| @@ -0,0 +1,79 @@ | |||
| 1 | +"""Public interface for stable MoE top-k expert selection.""" | ||
| 2 | + | ||
| 3 | +import os | ||
| 4 | + | ||
| 5 | +import torch | ||
| 6 | + | ||
| 7 | +from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source | ||
| 8 | +from cann_ops_tilelang.utils import align_up | ||
| 9 | + | ||
| 10 | +from .topk_gate_kernel import get_topk_gate_kernel | ||
| 11 | + | ||
| 12 | +_TOPK_MIN_CORES_ENV = 'OPS_TILELANG_TOPK_MIN_CORES' | ||
| 13 | + | ||
| 14 | +# Cache compiled kernels to bypass JIT decorator overhead on every call. | ||
| 15 | +# Key: (num_experts, num_topk, num_cores). Value: compiled kernel callable. | ||
| 16 | +_kernel_cache: dict = {} | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +def _get_topk_min_cores(num_vector_cores: int) -> int: | ||
| 20 | + default = max(1, num_vector_cores // 2) | ||
| 21 | + raw = os.getenv(_TOPK_MIN_CORES_ENV) | ||
| 22 | + if raw is None: | ||
| 23 | + return default | ||
| 24 | + try: | ||
| 25 | + return max(1, min(int(raw), num_vector_cores)) | ||
| 26 | + except ValueError: | ||
| 27 | + return default | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +def topk_gate(scores: torch.Tensor, num_topk: int) -> torch.Tensor: | ||
| 31 | + """Select stable top-k expert indices for each token on Ascend. | ||
| 32 | + | ||
| 33 | + ``scores`` must be a contiguous FP32 NPU tensor with shape | ||
| 34 | + ``(num_tokens, num_experts)``. Equal scores prefer the lower expert index. | ||
| 35 | + """ | ||
| 36 | + | ||
| 37 | + if not isinstance(scores, torch.Tensor): | ||
| 38 | + raise TypeError('scores must be a torch.Tensor.') | ||
| 39 | + if not isinstance(num_topk, int) or isinstance(num_topk, bool): | ||
| 40 | + raise TypeError(f'num_topk must be an int, got {type(num_topk).__name__}.') | ||
| 41 | + require_ascend() | ||
| 42 | + if scores.device.type != 'npu': | ||
| 43 | + raise ValueError(f'scores must be an NPU tensor, got {scores.device}.') | ||
| 44 | + if scores.dtype != torch.float32: | ||
| 45 | + raise TypeError(f'scores must use torch.float32, got {scores.dtype}.') | ||
| 46 | + if scores.ndim != 2: | ||
| 47 | + raise ValueError('scores must have shape (num_tokens, num_experts).') | ||
| 48 | + if not scores.is_contiguous(): | ||
| 49 | + raise ValueError('scores must be contiguous.') | ||
| 50 | + | ||
| 51 | + num_tokens, num_experts = scores.shape | ||
| 52 | + if num_topk <= 0 or num_topk > num_experts: | ||
| 53 | + raise ValueError(f'num_topk must be in [1, {num_experts}], got {num_topk}.') | ||
| 54 | + | ||
| 55 | + # The padded backing allocation lets the kernel issue whole-tile stores. | ||
| 56 | + topk_idx = torch.empty( | ||
| 57 | + (align_up(num_tokens, 8), num_topk), | ||
| 58 | + dtype=torch.int64, | ||
| 59 | + device=scores.device, | ||
| 60 | + )[:num_tokens] | ||
| 61 | + if num_tokens == 0: | ||
| 62 | + return topk_idx | ||
| 63 | + | ||
| 64 | + num_vector_cores = get_num_vector_cores() | ||
| 65 | + num_cores = min(num_vector_cores, num_tokens) | ||
| 66 | + min_cores = _get_topk_min_cores(num_vector_cores) | ||
| 67 | + if 1 < num_cores < min_cores: | ||
| 68 | + num_cores = min_cores | ||
| 69 | + | ||
| 70 | + cache_key = (num_experts, num_topk, num_cores) | ||
| 71 | + kernel = _kernel_cache.get(cache_key) | ||
| 72 | + if kernel is None: | ||
| 73 | + kernel = get_topk_gate_kernel(num_experts, num_topk, num_cores) | ||
| 74 | + _kernel_cache[cache_key] = kernel | ||
| 75 | + | ||
| 76 | + if should_print_kernel_source(): | ||
| 77 | + print(kernel.get_kernel_source()) | ||
| 78 | + kernel(scores, topk_idx) | ||
| 79 | + return topk_idx | ||
| @@ -0,0 +1,283 @@ | |||
| 1 | +import tilelang | ||
| 2 | +from tilelang import language as T | ||
| 3 | +from tilelang.language import simd as S | ||
| 4 | + | ||
| 5 | +from cann_ops_tilelang.config import get_num_vector_cores | ||
| 6 | +from cann_ops_tilelang.utils import align_up, ceil_div | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +@tilelang.jit | ||
| 10 | +def _get_topk_gate_kernel_impl(num_experts: int, num_topk: int, num_cores: int): | ||
| 11 | + """Top-k expert selection per token for Ascend NPU. | ||
| 12 | + | ||
| 13 | + Returns indices of the largest num_topk scores per row (ties broken by | ||
| 14 | + smaller index). Three compile-time specialized select_rows variants are | ||
| 15 | + selected based on num_experts: merged two-row (<=192 experts), single-row | ||
| 16 | + (<=384), or multi-group (>384). Output is int64 indices. | ||
| 17 | + """ | ||
| 18 | + # ---------- Compile-time constants and hardware adaptation ---------- | ||
| 19 | + num_stages = 6 | ||
| 20 | + vector_length = 64 | ||
| 21 | + | ||
| 22 | + # Grouping: split experts into groups that fit integer vector registers. | ||
| 23 | + max_vregs_per_group = 6 | ||
| 24 | + num_vregs_per_group = ceil_div(min(num_experts, max_vregs_per_group * vector_length), vector_length) | ||
| 25 | + experts_per_group = num_vregs_per_group * vector_length | ||
| 26 | + num_groups = ceil_div(num_experts, experts_per_group) | ||
| 27 | + if num_groups == 2: | ||
| 28 | + # 2-group layouts break the in-UB masking cycle: every pick after the | ||
| 29 | + # first repeats the initial argmax. Widening to >=3 groups of 3 vregs | ||
| 30 | + # runs the proven multi-group structure (verified at e=769/1025). | ||
| 31 | + num_vregs_per_group = ceil_div(num_vregs_per_group, 2) | ||
| 32 | + experts_per_group = num_vregs_per_group * vector_length | ||
| 33 | + num_groups = ceil_div(num_experts, experts_per_group) | ||
| 34 | + num_aligned_experts = num_groups * experts_per_group | ||
| 35 | + | ||
| 36 | + # Tiling: single group (<=384 experts) loads 8 rows at once. Multi-group | ||
| 37 | + # tile_rows widens just enough that each row burst is >=32B and 32B-aligned | ||
| 38 | + # (num_topk < 4 rows are 8/16/24B; sub-32B MTE writes must be 32B aligned). | ||
| 39 | + if num_groups == 1: | ||
| 40 | + tile_rows = 8 | ||
| 41 | + elif num_topk >= 4: | ||
| 42 | + tile_rows = 1 | ||
| 43 | + else: | ||
| 44 | + tile_rows = 4 if num_topk % 2 else 2 | ||
| 45 | + # acc_ub stores top‑k indices as int32; output is int64, so each index uses 2 int32 slots. | ||
| 46 | + acc_size = align_up(tile_rows * num_topk * 2, 32) | ||
| 47 | + acc_valid = tile_rows * num_topk * 2 | ||
| 48 | + inner_unroll = min(tile_rows, 2) | ||
| 49 | + outer_iters = tile_rows // inner_unroll | ||
| 50 | + | ||
| 51 | + num_tokens = T.dynamic('num_tokens') | ||
| 52 | + | ||
| 53 | + # ---------- Utility macros ---------- | ||
| 54 | + def _tree_reduce(count, mask, op, value_fn): | ||
| 55 | + """Balanced reduction tree over `count` values built by value_fn. | ||
| 56 | + Called from traced kernel body; no TIR loop constructs inside. | ||
| 57 | + """ | ||
| 58 | + current = [value_fn(i) for i in range(count)] | ||
| 59 | + while len(current) > 1: | ||
| 60 | + nxt = [] | ||
| 61 | + for j in range(0, len(current) - 1, 2): | ||
| 62 | + nxt.append(op(current[j], current[j + 1], mask)) | ||
| 63 | + if len(current) % 2: | ||
| 64 | + nxt.append(current[-1]) | ||
| 65 | + current = nxt | ||
| 66 | + return current[0] | ||
| 67 | + | ||
| 68 | + @T.macro | ||
| 69 | + def _simd_prologue(): | ||
| 70 | + """Initialize vector constants: all‑true mask, lane‑1 mask, -inf, int32 max, expert count.""" | ||
| 71 | + mask_32b = S.pset(32, 'PAT_ALL') | ||
| 72 | + one = S.pset(32, 'PAT_VL1') | ||
| 73 | + neg_inf = S.vdup(-T.infinity(T.float32), T.float32, mask_32b) | ||
| 74 | + int_max = S.vdup(T.max_value(T.int32), T.int32, mask_32b) | ||
| 75 | + num_exp = S.vdup(T.int32(num_experts), T.int32, mask_32b) | ||
| 76 | + return mask_32b, one, neg_inf, int_max, num_exp | ||
| 77 | + | ||
| 78 | + @T.macro | ||
| 79 | + def _select_one_k(scores_vec, index_vec, acc_ub, slot, mask_32b, one, neg_inf, int_max): | ||
| 80 | + """Single pick: find max (tie‑break by smallest index), store to acc_ub, | ||
| 81 | + then mask the chosen expert out with -inf. | ||
| 82 | + """ | ||
| 83 | + acc_max = _tree_reduce( | ||
| 84 | + num_vregs_per_group, mask_32b, lambda a, b, m: S.vmax(a, b, m), lambda vi: scores_vec[vi] | ||
| 85 | + ) | ||
| 86 | + mx = S.vdupv(S.vcmax(acc_max, mask_32b), mask_32b) | ||
| 87 | + acc_min_idx = _tree_reduce( | ||
| 88 | + num_vregs_per_group, | ||
| 89 | + mask_32b, | ||
| 90 | + lambda a, b, m: S.vmin(a, b, m), | ||
| 91 | + lambda vi: S.vsel(index_vec[vi], int_max, S.vcmp(scores_vec[vi], mx, mask_32b, 'eq')), | ||
| 92 | + ) | ||
| 93 | + idx = S.vdupv(S.vcmin(acc_min_idx, mask_32b), mask_32b) | ||
| 94 | + S.vsts(acc_ub[slot], idx, one, 'ONEPT_B32') | ||
| 95 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 96 | + scores_vec[i] = S.vsel(neg_inf, scores_vec[i], S.vcmp(index_vec[i], idx, mask_32b, 'eq')) | ||
| 97 | + | ||
| 98 | + @T.macro | ||
| 99 | + def _load_group(scores_vec, index_vec, scores_ub, row, group_id, neg_inf, num_exp, mask_32b): | ||
| 100 | + """Load one expert group from UB into registers, masking out invalid padding.""" | ||
| 101 | + base = group_id * experts_per_group | ||
| 102 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 103 | + offset = base + i * vector_length | ||
| 104 | + index_vec[i] = S.vci(T.int32(offset), T.int32) | ||
| 105 | + scores_vec[i] = S.vsel( | ||
| 106 | + S.vld(scores_ub[row, offset]), neg_inf, S.vcmp(index_vec[i], num_exp, mask_32b, 'lt') | ||
| 107 | + ) | ||
| 108 | + | ||
| 109 | + # ---------- Three compile‑time specialized select_rows variants ---------- | ||
| 110 | + if num_groups == 1: | ||
| 111 | + if num_vregs_per_group <= 3: | ||
| 112 | + # Variant 1: merged two‑row (≤192 experts). Process two rows per iteration. | ||
| 113 | + @T.macro | ||
| 114 | + def select_rows(base_row, scores_ub, acc_ub): | ||
| 115 | + for tr2 in T.serial(outer_iters): | ||
| 116 | + with T.SimdVF(): | ||
| 117 | + mask_32b, one, neg_inf, int_max, num_exp = _simd_prologue() | ||
| 118 | + | ||
| 119 | + sv0 = S.alloc_local((num_vregs_per_group,), T.float32) | ||
| 120 | + iv0 = S.alloc_local((num_vregs_per_group,), T.int32) | ||
| 121 | + sv1 = S.alloc_local((num_vregs_per_group,), T.float32) | ||
| 122 | + iv1 = S.alloc_local((num_vregs_per_group,), T.int32) | ||
| 123 | + | ||
| 124 | + # Load two rows and mask invalid lanes. | ||
| 125 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 126 | + sv0[i] = S.vld(scores_ub[tr2 * 2, i * vector_length]) | ||
| 127 | + iv0[i] = S.vci(T.int32(i * vector_length), T.int32) | ||
| 128 | + sv1[i] = S.vld(scores_ub[tr2 * 2 + 1, i * vector_length]) | ||
| 129 | + iv1[i] = S.vci(T.int32(i * vector_length), T.int32) | ||
| 130 | + sv0[i] = S.vsel(sv0[i], neg_inf, S.vcmp(iv0[i], num_exp, mask_32b, 'lt')) | ||
| 131 | + sv1[i] = S.vsel(sv1[i], neg_inf, S.vcmp(iv1[i], num_exp, mask_32b, 'lt')) | ||
| 132 | + | ||
| 133 | + # Interleave top‑k selection for both rows. | ||
| 134 | + for k in T.serial(num_topk): | ||
| 135 | + _select_one_k( | ||
| 136 | + sv0, iv0, acc_ub, (tr2 * 2) * num_topk * 2 + 2 * k, mask_32b, one, neg_inf, int_max | ||
| 137 | + ) | ||
| 138 | + _select_one_k( | ||
| 139 | + sv1, iv1, acc_ub, (tr2 * 2 + 1) * num_topk * 2 + 2 * k, mask_32b, one, neg_inf, int_max | ||
| 140 | + ) | ||
| 141 | + else: | ||
| 142 | + # Variant 2: single‑row vectorized (192 < experts ≤384). Process one row at a time. | ||
| 143 | + @T.macro | ||
| 144 | + def select_rows(base_row, scores_ub, acc_ub): | ||
| 145 | + for tr in T.serial(tile_rows): | ||
| 146 | + row = base_row + tr | ||
| 147 | + if row < num_tokens: | ||
| 148 | + with T.SimdVF(): | ||
| 149 | + mask_32b, one, neg_inf, int_max, num_exp = _simd_prologue() | ||
| 150 | + scores_vec = S.alloc_local((num_vregs_per_group,), T.float32) | ||
| 151 | + index_vec = S.alloc_local((num_vregs_per_group,), T.int32) | ||
| 152 | + | ||
| 153 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 154 | + scores_vec[i] = S.vld(scores_ub[tr, i * vector_length]) | ||
| 155 | + index_vec[i] = S.vci(T.int32(i * vector_length), T.int32) | ||
| 156 | + | ||
| 157 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 158 | + scores_vec[i] = S.vsel( | ||
| 159 | + scores_vec[i], neg_inf, S.vcmp(index_vec[i], num_exp, mask_32b, 'lt') | ||
| 160 | + ) | ||
| 161 | + | ||
| 162 | + for k in T.serial(num_topk): | ||
| 163 | + _select_one_k( | ||
| 164 | + scores_vec, | ||
| 165 | + index_vec, | ||
| 166 | + acc_ub, | ||
| 167 | + tr * num_topk * 2 + 2 * k, | ||
| 168 | + mask_32b, | ||
| 169 | + one, | ||
| 170 | + neg_inf, | ||
| 171 | + int_max, | ||
| 172 | + ) | ||
| 173 | + else: | ||
| 174 | + # Variant 3: multi‑group (>384 experts). Experts span multiple groups; process one row at a time. | ||
| 175 | + @T.macro | ||
| 176 | + def select_rows(base_row, scores_ub, acc_ub): | ||
| 177 | + for tr in T.serial(tile_rows): | ||
| 178 | + row = base_row + tr | ||
| 179 | + if row < num_tokens: | ||
| 180 | + with T.SimdVF(): | ||
| 181 | + mask_32b, one, neg_inf, int_max, num_exp = _simd_prologue() | ||
| 182 | + scores_vec = S.alloc_local((num_vregs_per_group,), T.float32) | ||
| 183 | + index_vec = S.alloc_local((num_vregs_per_group,), T.int32) | ||
| 184 | + tmp_idx_vec = S.alloc_local((num_vregs_per_group,), T.int32) | ||
| 185 | + acc = S.alloc_var(T.float32) | ||
| 186 | + acc_idx = S.alloc_var(T.int32) | ||
| 187 | + | ||
| 188 | + for k in T.serial(num_topk): | ||
| 189 | + # ---- Step 1: scan all groups to find global maximum ---- | ||
| 190 | + acc = neg_inf | ||
| 191 | + for group_id in T.serial(num_groups): | ||
| 192 | + _load_group(scores_vec, index_vec, scores_ub, tr, group_id, neg_inf, num_exp, mask_32b) | ||
| 193 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 194 | + acc = S.vmax(acc, scores_vec[i], mask_32b) | ||
| 195 | + mx = S.vdupv(S.vcmax(acc, mask_32b), mask_32b) | ||
| 196 | + | ||
| 197 | + # ---- Step 2: scan all groups to find smallest index among maxima ---- | ||
| 198 | + acc_idx = int_max | ||
| 199 | + for group_id in T.serial(num_groups): | ||
| 200 | + _load_group(scores_vec, index_vec, scores_ub, tr, group_id, neg_inf, num_exp, mask_32b) | ||
| 201 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 202 | + tmp_idx_vec[i] = S.vsel( | ||
| 203 | + index_vec[i], int_max, S.vcmp(scores_vec[i], mx, mask_32b, 'eq') | ||
| 204 | + ) | ||
| 205 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 206 | + acc_idx = S.vmin(acc_idx, tmp_idx_vec[i], mask_32b) | ||
| 207 | + idx = S.vdupv(S.vcmin(acc_idx, mask_32b), mask_32b) | ||
| 208 | + | ||
| 209 | + # ---- Store result ---- | ||
| 210 | + S.vsts(acc_ub[tr * num_topk * 2 + 2 * k], idx, one, 'ONEPT_B32') | ||
| 211 | + | ||
| 212 | + # ---- Step 3: mask out the selected expert (write back to scores_ub) ---- | ||
| 213 | + for group_id in T.serial(num_groups): | ||
| 214 | + base = group_id * experts_per_group | ||
| 215 | + for i in T.unroll(num_vregs_per_group, explicit=True): | ||
| 216 | + offset = base + i * vector_length | ||
| 217 | + index_vec[i] = S.vci(T.int32(offset), T.int32) | ||
| 218 | + scores_vec[i] = S.vld(scores_ub[tr, offset]) | ||
| 219 | + scores_vec[i] = S.vsel( | ||
| 220 | + neg_inf, scores_vec[i], S.vcmp(index_vec[i], idx, mask_32b, 'eq') | ||
| 221 | + ) | ||
| 222 | + S.vsts(scores_ub[tr, offset], scores_vec[i], mask_32b) | ||
| 223 | + | ||
| 224 | + S.mem_bar('VST_VLD') | ||
| 225 | + | ||
| 226 | + # ---------- Main kernel ---------- | ||
| 227 | + @T.prim_func | ||
| 228 | + def topk_gate_kernel( | ||
| 229 | + scores: T.Tensor[(num_tokens, num_experts), T.float32], | ||
| 230 | + topk_idx: T.Tensor[(num_tokens, num_topk), T.int64], | ||
| 231 | + ): | ||
| 232 | + with T.Kernel(num_cores) as core_id: | ||
| 233 | + # Allocate shared memory (UB) buffers | ||
| 234 | + scores_ub = T.alloc_shared((tile_rows, num_aligned_experts), T.float32) | ||
| 235 | + acc_ub = T.alloc_shared((acc_size,), T.int32) | ||
| 236 | + topk_idx_flat = T.view(topk_idx, (num_tokens * num_topk * 2,), T.int32) | ||
| 237 | + T.annotate_buffer_versions({scores_ub: num_stages, acc_ub: num_stages}) | ||
| 238 | + | ||
| 239 | + with T.SimdVF(): | ||
| 240 | + T.clear(acc_ub) # zero out all versions; ensures high 32 bits are 0 | ||
| 241 | + | ||
| 242 | + num_outer_iters = T.ceildiv(num_tokens, tile_rows * num_cores) | ||
| 243 | + # Software-pipelined main loop: overlap load, compute, store | ||
| 244 | + for w in T.Pipelined( | ||
| 245 | + num_outer_iters, | ||
| 246 | + num_stages=num_stages, | ||
| 247 | + annotations={'multi_buffer_eligible': [scores_ub, acc_ub]}, | ||
| 248 | + ): | ||
| 249 | + base_row = w * tile_rows * num_cores + core_id * tile_rows | ||
| 250 | + | ||
| 251 | + # ---- Load stage: GM -> UB ---- | ||
| 252 | + for tr in T.unroll(tile_rows, explicit=True): | ||
| 253 | + row = base_row + tr | ||
| 254 | + if row < num_tokens: | ||
| 255 | + T.copy(scores[row, :], scores_ub[tr, :num_experts]) | ||
| 256 | + | ||
| 257 | + # ---- Compute stage: top‑k selection within UB ---- | ||
| 258 | + select_rows(base_row, scores_ub, acc_ub) | ||
| 259 | + | ||
| 260 | + # ---- Store stage: UB -> GM ---- | ||
| 261 | + # Whole-tile bursts only: sub-32B GM writes (num_topk < 4 row | ||
| 262 | + # tails) hit "MTE non-atomic address not aligned" and fault the | ||
| 263 | + # core. The wrapper pads the output so every started tile is | ||
| 264 | + # whole; pad rows hold garbage and are dropped by the view. | ||
| 265 | + if base_row < num_tokens: | ||
| 266 | + T.copy( | ||
| 267 | + acc_ub[:acc_valid], | ||
| 268 | + topk_idx_flat[base_row * num_topk * 2 : base_row * num_topk * 2 + acc_valid], | ||
| 269 | + ) | ||
| 270 | + | ||
| 271 | + return topk_gate_kernel | ||
| 272 | + | ||
| 273 | + | ||
| 274 | +def get_topk_gate_kernel(num_experts: int, num_topk: int, num_cores: int = 0): | ||
| 275 | + """Build a kernel with num_cores resolved from the live runtime config. | ||
| 276 | + | ||
| 277 | + The @tilelang.jit impl is keyed on the concrete num_cores value, so the | ||
| 278 | + resolution must happen here per call: resolving the sentinel inside the | ||
| 279 | + traced body would be frozen into the first compiled specialization. | ||
| 280 | + """ | ||
| 281 | + if num_cores <= 0: | ||
| 282 | + num_cores = get_num_vector_cores() | ||
| 283 | + return _get_topk_gate_kernel_impl(num_experts, num_topk, num_cores) | ||
| @@ -0,0 +1,17 @@ | |||
| 1 | +import torch | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +def normalize_weight(topk_weights: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | ||
| 5 | + """Normalize each token's top-k weights so they sum to one. | ||
| 6 | + | ||
| 7 | + Args: | ||
| 8 | + topk_weights: Float32 tensor of shape ``(num_tokens, num_topk)``. | ||
| 9 | + | ||
| 10 | + Returns: | ||
| 11 | + Tuple of ``(denominator, normalized_weights)`` where *denominator* has | ||
| 12 | + shape ``(num_tokens,)`` and *normalized_weights* has the same shape as | ||
| 13 | + the input. | ||
| 14 | + """ | ||
| 15 | + denominator = torch.sum(topk_weights, dim=1) | ||
| 16 | + normalized_weights = topk_weights / denominator.unsqueeze(1) | ||
| 17 | + return denominator, normalized_weights | ||
| @@ -0,0 +1,381 @@ | |||
| 1 | +import os | ||
| 2 | + | ||
| 3 | +import pytest | ||
| 4 | +import torch | ||
| 5 | + | ||
| 6 | +import cann_ops_tilelang | ||
| 7 | +from cann_ops_tilelang.config import get_device | ||
| 8 | +from cann_ops_tilelang.moe.scoring import ScoringFunc | ||
| 9 | +from tests.moe.topk_ref import moe_topk_gate as torch_moe_topk_gate | ||
| 10 | +from tests.testing.bench import make_param_id | ||
| 11 | +from tests.testing.generator import generate_num_tokens, get_test_level | ||
| 12 | +from tests.testing.numeric import assert_equal, count_bytes | ||
| 13 | + | ||
| 14 | +# Disable TileLang prints | ||
| 15 | +os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0' | ||
| 16 | +pytestmark = pytest.mark.npu | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +_CORE_CONFIGS = [ | ||
| 20 | + (256, 2, 8), # oss-ignore-note: V3 free | ||
| 21 | + (256, 1, 6), # oss-ignore-line: V33 infra V1 | ||
| 22 | + (384, 1, 6), # oss-ignore-line: V33 full | ||
| 23 | +] | ||
| 24 | + | ||
| 25 | +_FULL_CONFIGS = [ | ||
| 26 | + (72, 1, 6), # oss-ignore-note: V3 lite | ||
| 27 | + (32, 2, 6), # oss-ignore-note: V3 MM 0 | ||
| 28 | + (64, 2, 6), # oss-ignore-note: V3 MM 1 | ||
| 29 | + (96, 2, 6), # oss-ignore-note: V3 MM 2 | ||
| 30 | + (16, 2, 6), # oss-ignore-note: V3 MM 3 | ||
| 31 | + (36, 2, 6), # oss-ignore-note: V3 MM 4 | ||
| 32 | + (108, 2, 6), # oss-ignore-note: V3 MM 5 | ||
| 33 | + (128, 2, 6), # oss-ignore-note: V3 MM 6 | ||
| 34 | + (144, 2, 6), # oss-ignore-note: V3 MM 7 | ||
| 35 | + (256, 2, 8), # oss-ignore-note: V3 free | ||
| 36 | + (8, 1, 2), # oss-ignore-line: V33 lite | ||
| 37 | + (128, 1, 4), # oss-ignore-line: V33 mid | ||
| 38 | + (384, 1, 6), # oss-ignore-line: V33 full | ||
| 39 | + (256, 1, 6), # oss-ignore-line: V33 infra V1 | ||
| 40 | +] | ||
| 41 | + | ||
| 42 | + | ||
| 43 | +def generate_test_params(level: int) -> list[dict]: | ||
| 44 | + configs = _CORE_CONFIGS if level == 0 else _FULL_CONFIGS | ||
| 45 | + params = [ | ||
| 46 | + { | ||
| 47 | + 'num_tokens': num_tokens, | ||
| 48 | + 'num_padded_tokens': num_padded_tokens, | ||
| 49 | + 'num_topk': num_topk, | ||
| 50 | + 'num_routed_experts': num_routed_experts, | ||
| 51 | + 'num_shared_experts': num_shared_experts, | ||
| 52 | + 'scoring_func': sf, | ||
| 53 | + 'with_bias': with_bias, | ||
| 54 | + 'with_image_bias': with_image_bias, | ||
| 55 | + 'with_force_random': with_force_random, | ||
| 56 | + 'use_shared_as_routed': use_shared_as_routed, | ||
| 57 | + } | ||
| 58 | + for num_tokens in generate_num_tokens(level) | ||
| 59 | + for num_padded_tokens in (0, 10) | ||
| 60 | + for num_routed_experts, num_shared_experts, num_topk in configs | ||
| 61 | + for sf in ScoringFunc | ||
| 62 | + for with_bias in ([True] if level == 0 else [True, False]) | ||
| 63 | + for with_image_bias in ([True] if level == 0 else [True, False]) | ||
| 64 | + for with_force_random in ([True] if level == 0 else [True, False]) | ||
| 65 | + for use_shared_as_routed in ( | ||
| 66 | + (False, True) | ||
| 67 | + if num_topk % num_shared_experts == 0 and num_routed_experts % (num_topk // num_shared_experts) == 0 | ||
| 68 | + else (False,) | ||
| 69 | + ) | ||
| 70 | + ] | ||
| 71 | + return params | ||
| 72 | + | ||
| 73 | + | ||
| 74 | +# noinspection PyShadowingNames | ||
| 75 | +def get_kwargs(params, fix_routing): | ||
| 76 | + num_tokens, num_padded_tokens = params['num_tokens'], params['num_padded_tokens'] | ||
| 77 | + num_routed_experts = params['num_routed_experts'] | ||
| 78 | + num_shared_experts = params['num_shared_experts'] | ||
| 79 | + use_shared_as_routed = params['use_shared_as_routed'] | ||
| 80 | + num_topk = params['num_topk'] | ||
| 81 | + with_bias, with_image_bias = params['with_bias'], params['with_image_bias'] | ||
| 82 | + with_force_random = params['with_force_random'] | ||
| 83 | + mask = None | ||
| 84 | + if num_padded_tokens > 0: | ||
| 85 | + mask = torch.ones(num_tokens + num_padded_tokens, dtype=torch.bool, device=get_device()) | ||
| 86 | + mask[-num_padded_tokens:] = False | ||
| 87 | + | ||
| 88 | + to_physical_map = None | ||
| 89 | + logical_count = None | ||
| 90 | + fix_routing_mask = None | ||
| 91 | + force_random = None | ||
| 92 | + unmapped_topk_idx = torch.zeros((num_tokens + num_padded_tokens, num_topk), dtype=torch.int64, device=get_device()) | ||
| 93 | + | ||
| 94 | + bias = torch.randn(num_routed_experts, dtype=torch.float, device=get_device()) if with_bias else None | ||
| 95 | + image_bias = torch.randn(num_routed_experts, dtype=torch.float, device=get_device()) if with_image_bias else None | ||
| 96 | + image_token_mask = ( | ||
| 97 | + torch.rand(num_tokens + num_padded_tokens, dtype=torch.float, device=get_device()) < 0.5 | ||
| 98 | + if with_image_bias | ||
| 99 | + else None | ||
| 100 | + ) | ||
| 101 | + | ||
| 102 | + if use_shared_as_routed: | ||
| 103 | + to_physical_map = torch.arange( | ||
| 104 | + 0, | ||
| 105 | + num_routed_experts + num_shared_experts, | ||
| 106 | + dtype=torch.int, | ||
| 107 | + device=get_device(), | ||
| 108 | + ) | ||
| 109 | + to_physical_map = to_physical_map.view(-1, 1).contiguous() | ||
| 110 | + logical_count = torch.ones((num_routed_experts + num_shared_experts,), dtype=torch.int, device=get_device()) | ||
| 111 | + | ||
| 112 | + if fix_routing: | ||
| 113 | + fix_routing_mask = torch.ones((num_tokens + num_padded_tokens,), dtype=torch.bool, device=get_device()) | ||
| 114 | + unmapped_topk_idx = torch.randint( | ||
| 115 | + 0, | ||
| 116 | + num_routed_experts, | ||
| 117 | + (num_tokens + num_padded_tokens, num_topk), | ||
| 118 | + dtype=torch.int64, | ||
| 119 | + device=get_device(), | ||
| 120 | + ) | ||
| 121 | + | ||
| 122 | + if with_force_random: | ||
| 123 | + force_random = torch.rand(num_tokens + num_padded_tokens, device=get_device()) < 0.5 | ||
| 124 | + # Force random do not appear with fix_routing, which leads to conflict | ||
| 125 | + if fix_routing: | ||
| 126 | + force_random = torch.logical_and(torch.logical_not(fix_routing_mask), force_random) | ||
| 127 | + | ||
| 128 | + return dict( | ||
| 129 | + mask=mask, | ||
| 130 | + bias=bias, | ||
| 131 | + image_bias=image_bias, | ||
| 132 | + image_token_mask=image_token_mask, | ||
| 133 | + fix_routing_mask=fix_routing_mask, | ||
| 134 | + to_physical_map=to_physical_map, | ||
| 135 | + logical_count=logical_count, | ||
| 136 | + unmapped_topk_idx=unmapped_topk_idx, | ||
| 137 | + force_random=force_random, | ||
| 138 | + ) | ||
| 139 | + | ||
| 140 | + | ||
| 141 | +def assert_uniform_distribution(values: torch.Tensor, num_bins: int, description: str): | ||
| 142 | + """Assert that values are approximately uniformly distributed across [0, num_bins) using chi-squared test.""" | ||
| 143 | + if values.numel() < num_bins * 10: | ||
| 144 | + return | ||
| 145 | + counts = torch.bincount(values.to(torch.int64), minlength=num_bins).float() | ||
| 146 | + expected = values.numel() / num_bins | ||
| 147 | + chi_squared = ((counts - expected) ** 2 / expected).sum().item() | ||
| 148 | + # Use a lenient threshold: chi-squared < 4 * degrees_of_freedom | ||
| 149 | + # (p-value threshold is very loose to avoid flaky tests) | ||
| 150 | + degrees_of_freedom = num_bins - 1 | ||
| 151 | + assert chi_squared < 4 * degrees_of_freedom, ( | ||
| 152 | + f'Force random expert distribution is not uniform ({description}): ' | ||
| 153 | + f'chi_squared={chi_squared:.2f}, threshold={4 * degrees_of_freedom}, ' | ||
| 154 | + f'counts={counts.tolist()}, expected={expected:.1f}' | ||
| 155 | + ) | ||
| 156 | + | ||
| 157 | + | ||
| 158 | +@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id) | ||
| 159 | +def test_moe_topk_gate(params): | ||
| 160 | + num_tokens, num_padded_tokens = params['num_tokens'], params['num_padded_tokens'] | ||
| 161 | + num_routed_experts = params['num_routed_experts'] | ||
| 162 | + num_shared_experts = params['num_shared_experts'] | ||
| 163 | + num_topk = params['num_topk'] | ||
| 164 | + scoring_func = params['scoring_func'] | ||
| 165 | + use_shared_as_routed = params['use_shared_as_routed'] | ||
| 166 | + | ||
| 167 | + routed_scaling_factor = 1.5 | ||
| 168 | + ep_rank = 0 | ||
| 169 | + | ||
| 170 | + # Correctness test | ||
| 171 | + for fix_routing in (False, True): | ||
| 172 | + logits = torch.randn( | ||
| 173 | + (num_tokens + num_padded_tokens, num_routed_experts), dtype=torch.float, device=get_device() | ||
| 174 | + ) | ||
| 175 | + args = ( | ||
| 176 | + logits, | ||
| 177 | + num_topk, | ||
| 178 | + use_shared_as_routed, | ||
| 179 | + num_shared_experts, | ||
| 180 | + routed_scaling_factor, | ||
| 181 | + ep_rank, | ||
| 182 | + str(scoring_func), | ||
| 183 | + ) | ||
| 184 | + kwargs = get_kwargs(params, fix_routing) | ||
| 185 | + | ||
| 186 | + def clone(tensor_dict: dict): | ||
| 187 | + return {k: v.clone() if k == 'unmapped_topk_idx' else v for (k, v) in tensor_dict.items()} | ||
| 188 | + | ||
| 189 | + kwargs_ref = clone(kwargs) | ||
| 190 | + kwargs_out = clone(kwargs) | ||
| 191 | + | ||
| 192 | + topk_idx, topk_weights = cann_ops_tilelang.moe_topk_gate(*args, **kwargs) | ||
| 193 | + num_physical_topk = num_topk + num_shared_experts if use_shared_as_routed else num_topk | ||
| 194 | + topk_idx_out = torch.empty( | ||
| 195 | + (num_tokens + num_padded_tokens, num_physical_topk), dtype=torch.int64, device=get_device() | ||
| 196 | + ) | ||
| 197 | + topk_weights_out = torch.empty( | ||
| 198 | + num_tokens + num_padded_tokens, num_physical_topk, dtype=torch.float32, device=get_device() | ||
| 199 | + ) | ||
| 200 | + cann_ops_tilelang.moe_topk_gate(*args, **kwargs_out, out=(topk_idx_out, topk_weights_out)) | ||
| 201 | + | ||
| 202 | + assert_equal(topk_idx, topk_idx_out) | ||
| 203 | + assert_equal(topk_weights, topk_weights_out) | ||
| 204 | + assert_equal(kwargs['unmapped_topk_idx'], kwargs_out['unmapped_topk_idx']) | ||
| 205 | + | ||
| 206 | + # Compare against torch reference for tokens where force_random is False | ||
| 207 | + kwargs_ref_no_fr = {k: v for k, v in kwargs_ref.items() if k != 'force_random'} | ||
| 208 | + topk_idx_ref, topk_weights_ref = torch_moe_topk_gate(*args, **kwargs_ref_no_fr) | ||
| 209 | + | ||
| 210 | + # Mask to select tokens that are NOT force_random (i.e. should match ref) | ||
| 211 | + force_random_mask = kwargs['force_random'] | ||
| 212 | + mask = kwargs['mask'] | ||
| 213 | + logical_count = kwargs['logical_count'] | ||
| 214 | + if force_random_mask is not None: | ||
| 215 | + force_random_mask = torch.logical_and( | ||
| 216 | + force_random_mask, | ||
| 217 | + mask | ||
| 218 | + if mask is not None | ||
| 219 | + else torch.ones(num_tokens + num_padded_tokens, dtype=torch.bool, device=get_device()), | ||
| 220 | + ) | ||
| 221 | + compare_mask = ~force_random_mask | ||
| 222 | + else: | ||
| 223 | + compare_mask = torch.ones(topk_idx.size(0), dtype=torch.bool, device=get_device()) | ||
| 224 | + | ||
| 225 | + unmapped_topk_idx = kwargs['unmapped_topk_idx'] | ||
| 226 | + unmapped_topk_idx_ref = kwargs_ref['unmapped_topk_idx'] | ||
| 227 | + | ||
| 228 | + sorted_topk_idx, _ = topk_idx[compare_mask].sort(dim=1) | ||
| 229 | + sorted_topk_idx_ref, _ = topk_idx_ref[compare_mask].sort(dim=1) | ||
| 230 | + | ||
| 231 | + sorted_unmapped_topk_idx = unmapped_topk_idx[compare_mask].sort(dim=1)[0] | ||
| 232 | + sorted_unmapped_topk_idx_ref = unmapped_topk_idx_ref[compare_mask].sort(dim=1)[0] | ||
| 233 | + | ||
| 234 | + sorted_topk_weights, _ = topk_weights[compare_mask].sort(dim=1) | ||
| 235 | + sorted_topk_weights_ref, _ = topk_weights_ref[compare_mask].sort(dim=1) | ||
| 236 | + | ||
| 237 | + assert_equal(sorted_topk_idx, sorted_topk_idx_ref) | ||
| 238 | + assert_equal(sorted_unmapped_topk_idx, sorted_unmapped_topk_idx_ref) | ||
| 239 | + assert torch.allclose(sorted_topk_weights, sorted_topk_weights_ref), ( | ||
| 240 | + f'{sorted_topk_weights=}\n' | ||
| 241 | + f'{sorted_topk_weights_ref=}\n' | ||
| 242 | + f'{scoring_func=}, {num_routed_experts=}, {num_shared_experts=}, {num_topk=}, {fix_routing=}\n' | ||
| 243 | + 'Different topk weights: \n{}'.format( | ||
| 244 | + [ | ||
| 245 | + (sorted_topk_weights[i], sorted_topk_weights_ref[i]) | ||
| 246 | + for i in range(topk_weights.size(0)) | ||
| 247 | + if not torch.equal(sorted_topk_weights[i], sorted_topk_weights_ref[i]) | ||
| 248 | + ] | ||
| 249 | + ) | ||
| 250 | + ) | ||
| 251 | + | ||
| 252 | + # Check distribution for force random tokens | ||
| 253 | + if force_random_mask is not None and force_random_mask.any(): | ||
| 254 | + fr_topk_idx = topk_idx[force_random_mask] | ||
| 255 | + fr_topk_weights = topk_weights[force_random_mask] | ||
| 256 | + num_logical_experts = num_routed_experts + (num_shared_experts if use_shared_as_routed else 0) | ||
| 257 | + num_physical_experts = torch.sum(logical_count).item() if logical_count is not None else num_logical_experts | ||
| 258 | + | ||
| 259 | + # Weights should be positive | ||
| 260 | + if fr_topk_weights.numel() > 0: | ||
| 261 | + assert (fr_topk_weights > 0).all(), ( | ||
| 262 | + 'Force random tokens should have positive weights for valid experts, got: ' | ||
| 263 | + f'{fr_topk_weights[fr_topk_weights <= 0]}' | ||
| 264 | + ) | ||
| 265 | + | ||
| 266 | + # Validity: random experts should in range | ||
| 267 | + assert torch.all(torch.logical_and(fr_topk_idx < num_physical_experts, fr_topk_idx >= 0)).item() | ||
| 268 | + | ||
| 269 | + # Uniformity: random experts should be uniformly distributed | ||
| 270 | + assert_uniform_distribution(fr_topk_idx.view(-1), num_physical_experts, 'rank') | ||
| 271 | + | ||
| 272 | + # all -1 for unmapped | ||
| 273 | + assert torch.all(unmapped_topk_idx[force_random_mask] == -1).item() | ||
| 274 | + | ||
| 275 | + # Check sort stability | ||
| 276 | + if not params['with_force_random']: | ||
| 277 | + logits = torch.zeros( | ||
| 278 | + (num_tokens + num_padded_tokens, num_routed_experts), dtype=torch.float, device=get_device() | ||
| 279 | + ) | ||
| 280 | + args = ( | ||
| 281 | + logits, | ||
| 282 | + num_topk, | ||
| 283 | + use_shared_as_routed, | ||
| 284 | + num_shared_experts, | ||
| 285 | + routed_scaling_factor, | ||
| 286 | + ep_rank, | ||
| 287 | + str(ScoringFunc.SIGMOID), | ||
| 288 | + ) | ||
| 289 | + fix_routing = False | ||
| 290 | + kwargs = get_kwargs(params, fix_routing) | ||
| 291 | + kwargs['bias'], kwargs['image_bias'], kwargs['image_token_mask'] = None, None, None | ||
| 292 | + topk_idx, topk_weights = cann_ops_tilelang.moe_topk_gate(*args, **kwargs) | ||
| 293 | + assert torch.all( | ||
| 294 | + topk_idx[:num_tokens, :num_topk] == torch.arange(0, num_topk, dtype=topk_idx.dtype, device=topk_idx.device) | ||
| 295 | + ) | ||
| 296 | + | ||
| 297 | + | ||
| 298 | +def generate_benchmark_func(params): | ||
| 299 | + num_tokens, num_padded_tokens = params['num_tokens'], params['num_padded_tokens'] | ||
| 300 | + num_routed_experts = params['num_routed_experts'] | ||
| 301 | + routed_scaling_factor = 1.5 | ||
| 302 | + ep_rank = 0 | ||
| 303 | + fix_routing = False | ||
| 304 | + | ||
| 305 | + logits = torch.randn((num_tokens + num_padded_tokens, num_routed_experts), dtype=torch.float, device=get_device()) | ||
| 306 | + kwargs = get_kwargs(params, fix_routing) | ||
| 307 | + | ||
| 308 | + args = ( | ||
| 309 | + logits, | ||
| 310 | + params['num_topk'], | ||
| 311 | + params['use_shared_as_routed'], | ||
| 312 | + params['num_shared_experts'], | ||
| 313 | + routed_scaling_factor, | ||
| 314 | + ep_rank, | ||
| 315 | + str(params['scoring_func']), | ||
| 316 | + ) | ||
| 317 | + | ||
| 318 | + def run_moe_topk_gate(): | ||
| 319 | + return cann_ops_tilelang.moe_topk_gate(*args, **kwargs) | ||
| 320 | + | ||
| 321 | + num_bytes = count_bytes(logits) | ||
| 322 | + return run_moe_topk_gate, num_bytes | ||
| 323 | + | ||
| 324 | + | ||
| 325 | +def test_moe_topk_gate_multi_group(): | ||
| 326 | + """Experts beyond the first six SIMD vectors must participate in TopK.""" | ||
| 327 | + num_routed_experts = 448 | ||
| 328 | + logits = torch.zeros((1, num_routed_experts), dtype=torch.float32, device=get_device()) | ||
| 329 | + logits[0, -1] = 100.0 | ||
| 330 | + | ||
| 331 | + topk_idx, topk_weights = cann_ops_tilelang.moe_topk_gate( | ||
| 332 | + logits, | ||
| 333 | + num_topk=1, | ||
| 334 | + use_shared_as_routed=False, | ||
| 335 | + num_shared_experts=1, | ||
| 336 | + routed_scaling_factor=1.0, | ||
| 337 | + ep_rank=0, | ||
| 338 | + scoring_func='identity', | ||
| 339 | + ) | ||
| 340 | + | ||
| 341 | + assert_equal(topk_idx, torch.tensor([[447]], dtype=torch.int64, device=get_device())) | ||
| 342 | + assert_equal(topk_weights, torch.tensor([[1.0]], dtype=torch.float32, device=get_device())) | ||
| 343 | + | ||
| 344 | + | ||
| 345 | +def test_moe_topk_gate_force_random_is_unique(): | ||
| 346 | + num_tokens = 64 | ||
| 347 | + num_routed_experts = 256 | ||
| 348 | + num_topk = 8 | ||
| 349 | + logits = torch.zeros((num_tokens, num_routed_experts), dtype=torch.float32, device=get_device()) | ||
| 350 | + force_random = torch.ones((num_tokens,), dtype=torch.bool, device=get_device()) | ||
| 351 | + | ||
| 352 | + topk_idx, _ = cann_ops_tilelang.moe_topk_gate( | ||
| 353 | + logits, | ||
| 354 | + num_topk=num_topk, | ||
| 355 | + use_shared_as_routed=False, | ||
| 356 | + num_shared_experts=1, | ||
| 357 | + routed_scaling_factor=1.0, | ||
| 358 | + ep_rank=0, | ||
| 359 | + scoring_func='identity', | ||
| 360 | + force_random=force_random, | ||
| 361 | + ) | ||
| 362 | + | ||
| 363 | + sorted_idx = topk_idx.sort(dim=1).values | ||
| 364 | + assert torch.all(sorted_idx[:, 1:] != sorted_idx[:, :-1]) | ||
| 365 | + assert torch.all(torch.logical_and(topk_idx >= 0, topk_idx < num_routed_experts)) | ||
| 366 | + | ||
| 367 | + | ||
| 368 | +@pytest.mark.benchmark | ||
| 369 | +@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id) | ||
| 370 | +def test_moe_topk_gate_benchmark(benchmark_timer, benchmark_record, params): | ||
| 371 | + func, num_bytes = generate_benchmark_func(params) | ||
| 372 | + t_us = benchmark_timer(func) | ||
| 373 | + bandwidth_gbs = num_bytes / t_us / 1e3 | ||
| 374 | + | ||
| 375 | + benchmark_record( | ||
| 376 | + kernel='moe_topk_gate', | ||
| 377 | + operation='fwd', | ||
| 378 | + params=params, | ||
| 379 | + time_us=t_us, | ||
| 380 | + bandwidth_gbs=bandwidth_gbs, | ||
| 381 | + ) | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +import os | ||
| 2 | + | ||
| 3 | +import pytest | ||
| 4 | +import torch | ||
| 5 | + | ||
| 6 | +import cann_ops_tilelang | ||
| 7 | +from cann_ops_tilelang.config import get_device | ||
| 8 | +from tests.moe.normalize_weight_ref import normalize_weight as torch_normalize_weight | ||
| 9 | +from tests.testing.bench import make_param_id | ||
| 10 | +from tests.testing.generator import generate_moe_params, generate_topk_idx, get_test_level | ||
| 11 | +from tests.testing.numeric import count_bytes | ||
| 12 | + | ||
| 13 | +# Disable TileLang prints | ||
| 14 | +os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0' | ||
| 15 | +pytestmark = pytest.mark.npu | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +def generate_test_data(params): | ||
| 19 | + num_topk = params['num_topk'] | ||
| 20 | + | ||
| 21 | + topk_idx = generate_topk_idx(params) | ||
| 22 | + num_tokens = topk_idx.shape[0] | ||
| 23 | + topk_weights = torch.rand((num_tokens, num_topk), dtype=torch.float32, device=get_device()) | ||
| 24 | + | ||
| 25 | + return (topk_weights, num_tokens) | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +@pytest.mark.parametrize('params', list(generate_moe_params(get_test_level())), ids=make_param_id) | ||
| 29 | +def test_normalize_weight(params): | ||
| 30 | + topk_weights, _ = generate_test_data(params) | ||
| 31 | + | ||
| 32 | + denominator, normalized_weights = cann_ops_tilelang.normalize_weight(topk_weights) | ||
| 33 | + | ||
| 34 | + denom_ref, norm_ref = torch_normalize_weight(topk_weights) | ||
| 35 | + torch.testing.assert_close(denominator, denom_ref, atol=2e-6, rtol=0) | ||
| 36 | + torch.testing.assert_close(normalized_weights, norm_ref, atol=2e-6, rtol=0) | ||
| 37 | + | ||
| 38 | + | ||
| 39 | +@pytest.mark.benchmark | ||
| 40 | +@pytest.mark.parametrize('params', list(generate_moe_params(0)), ids=make_param_id) | ||
| 41 | +def test_normalize_weight_benchmark(benchmark_timer, benchmark_record, params): | ||
| 42 | + topk_weights, num_tokens = generate_test_data(params) | ||
| 43 | + | ||
| 44 | + denominator, normalized_weights = cann_ops_tilelang.normalize_weight(topk_weights) | ||
| 45 | + | ||
| 46 | + t_us = benchmark_timer(lambda: cann_ops_tilelang.normalize_weight(topk_weights)) | ||
| 47 | + num_bytes = count_bytes(topk_weights, denominator, normalized_weights) | ||
| 48 | + bandwidth_gbs = num_bytes / t_us / 1e3 | ||
| 49 | + | ||
| 50 | + params.pop('num_send_tokens') | ||
| 51 | + benchmark_record( | ||
| 52 | + kernel='normalize_weight', | ||
| 53 | + operation='fwd', | ||
| 54 | + params={'num_tokens': num_tokens, **params}, | ||
| 55 | + time_us=t_us, | ||
| 56 | + bandwidth_gbs=bandwidth_gbs, | ||
| 57 | + ) | ||
| @@ -0,0 +1,94 @@ | |||
| 1 | +import itertools | ||
| 2 | +import os | ||
| 3 | + | ||
| 4 | +import pytest | ||
| 5 | +import torch | ||
| 6 | + | ||
| 7 | +import cann_ops_tilelang | ||
| 8 | +from cann_ops_tilelang.config import get_device | ||
| 9 | +from tests.moe.topk_ref import stable_topk as torch_stable_topk | ||
| 10 | +from tests.testing.bench import make_param_id | ||
| 11 | +from tests.testing.generator import generate_num_tokens, get_test_level | ||
| 12 | +from tests.testing.numeric import assert_equal, count_bytes | ||
| 13 | + | ||
| 14 | +# Disable TileLang prints | ||
| 15 | +os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0' | ||
| 16 | +pytestmark = pytest.mark.npu | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +_EXPERT_CONFIGS = [ | ||
| 20 | + (72, 6), # oss-ignore-note: V3 lite | ||
| 21 | + (32, 6), # oss-ignore-note: V3 MM 0 | ||
| 22 | + (64, 6), # oss-ignore-note: V3 MM 1 | ||
| 23 | + (96, 6), # oss-ignore-note: V3 MM 2 | ||
| 24 | + (16, 6), # oss-ignore-note: V3 MM 3 | ||
| 25 | + (36, 6), # oss-ignore-note: V3 MM 4 | ||
| 26 | + (108, 6), # oss-ignore-note: V3 MM 5 | ||
| 27 | + (128, 6), # oss-ignore-note: V3 MM 6 | ||
| 28 | + (144, 6), # oss-ignore-note: V3 MM 7 | ||
| 29 | + (256, 8), # oss-ignore-note: V3 full | ||
| 30 | + (8, 2), # oss-ignore-line: V33 lite | ||
| 31 | + (128, 4), # oss-ignore-line: V33 mid | ||
| 32 | + (384, 6), # oss-ignore-line: V33 full | ||
| 33 | + (256, 6), # oss-ignore-line: V33 infra V1 | ||
| 34 | +] # fmt: off | ||
| 35 | + | ||
| 36 | + | ||
| 37 | +def generate_test_data(params): | ||
| 38 | + num_tokens = params['num_tokens'] | ||
| 39 | + num_experts = params['num_experts'] | ||
| 40 | + scores = torch.randn((num_tokens, num_experts), dtype=torch.float, device=get_device()) | ||
| 41 | + return scores | ||
| 42 | + | ||
| 43 | + | ||
| 44 | +def generate_test_params(level: int) -> list[dict]: | ||
| 45 | + return [ | ||
| 46 | + { | ||
| 47 | + 'num_tokens': num_tokens, | ||
| 48 | + 'num_experts': num_experts, | ||
| 49 | + 'num_topk': num_topk, | ||
| 50 | + } | ||
| 51 | + for num_tokens in generate_num_tokens(level) | ||
| 52 | + # oss-ignore-note: Researcher may use various combination of num_experts and num_topk, so we try to support a wide range of configurations # noqa: E501 | ||
| 53 | + for num_experts, num_topk in itertools.product([1, 35, 1025], [1, 9, 32]) | ||
| 54 | + if num_experts >= num_topk | ||
| 55 | + ] | ||
| 56 | + | ||
| 57 | + | ||
| 58 | +@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id) | ||
| 59 | +def test_topk_gate(params): | ||
| 60 | + scores = generate_test_data(params) | ||
| 61 | + num_topk = params['num_topk'] | ||
| 62 | + | ||
| 63 | + topk_idx_ref = torch_stable_topk(scores, num_topk) | ||
| 64 | + topk_idx = cann_ops_tilelang.topk_gate(scores, num_topk) | ||
| 65 | + assert_equal(topk_idx, topk_idx_ref) | ||
| 66 | + | ||
| 67 | + | ||
| 68 | +@pytest.mark.benchmark | ||
| 69 | +@pytest.mark.parametrize( | ||
| 70 | + 'params', | ||
| 71 | + [ | ||
| 72 | + {'num_tokens': num_tokens, 'num_experts': num_experts, 'num_topk': num_topk} | ||
| 73 | + for num_tokens in generate_num_tokens(0) | ||
| 74 | + for num_experts, num_topk in _EXPERT_CONFIGS | ||
| 75 | + ], | ||
| 76 | + ids=make_param_id, | ||
| 77 | +) | ||
| 78 | +def test_topk_gate_benchmark(benchmark_timer, benchmark_record, params): | ||
| 79 | + scores = generate_test_data(params) | ||
| 80 | + num_topk = params['num_topk'] | ||
| 81 | + | ||
| 82 | + topk_idx = cann_ops_tilelang.topk_gate(scores, num_topk) | ||
| 83 | + | ||
| 84 | + t_us = benchmark_timer(lambda: cann_ops_tilelang.topk_gate(scores, num_topk)) | ||
| 85 | + num_bytes = count_bytes(scores, topk_idx) | ||
| 86 | + bandwidth_gbs = num_bytes / t_us / 1e3 | ||
| 87 | + | ||
| 88 | + benchmark_record( | ||
| 89 | + kernel='topk_gate', | ||
| 90 | + operation='fwd', | ||
| 91 | + params=params, | ||
| 92 | + time_us=t_us, | ||
| 93 | + bandwidth_gbs=bandwidth_gbs, | ||
| 94 | + ) | ||
| @@ -0,0 +1,194 @@ | |||
| 1 | +import torch | ||
| 2 | +import torch.nn.functional as F | ||
| 3 | + | ||
| 4 | +from cann_ops_tilelang.moe.scoring import ScoringFunc | ||
| 5 | + | ||
| 6 | + | ||
| 7 | +def stable_topk(scores: torch.Tensor, num_topk: int) -> torch.Tensor: | ||
| 8 | + _, sorted_indices = torch.sort(scores, dim=1, descending=True, stable=True) | ||
| 9 | + topk_idx = sorted_indices[:, :num_topk].contiguous() | ||
| 10 | + return topk_idx.as_strided(topk_idx.shape, (num_topk, 1)) | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +def moe_topk_gate( | ||
| 14 | + logits: torch.Tensor, | ||
| 15 | + num_topk: int, | ||
| 16 | + use_shared_as_routed: bool, | ||
| 17 | + num_shared_experts: int, | ||
| 18 | + routed_scaling_factor: float, | ||
| 19 | + ep_rank: int, | ||
| 20 | + scoring_func: str, | ||
| 21 | + mask: torch.Tensor | None = None, | ||
| 22 | + bias: torch.Tensor | None = None, | ||
| 23 | + image_bias: torch.Tensor | None = None, | ||
| 24 | + image_token_mask: torch.Tensor | None = None, | ||
| 25 | + fix_routing_mask: torch.Tensor | None = None, | ||
| 26 | + to_physical_map: torch.Tensor | None = None, | ||
| 27 | + logical_count: torch.Tensor | None = None, | ||
| 28 | + unmapped_topk_idx: torch.Tensor | None = None, | ||
| 29 | + out: tuple[torch.Tensor, torch.Tensor] | None = None, | ||
| 30 | +) -> tuple[torch.Tensor, torch.Tensor]: | ||
| 31 | + """PyTorch reference for top-k expert routing. | ||
| 32 | + | ||
| 33 | + Args: | ||
| 34 | + logits: Raw token-expert logits, shape ``(num_tokens, num_routed_experts)``, | ||
| 35 | + ``float32``. | ||
| 36 | + bias: Optional per-expert bias added to scores before ranking, shape | ||
| 37 | + ``(num_routed_experts,)``, ``float32``. | ||
| 38 | + num_topk: Number of routed experts to select per token. | ||
| 39 | + use_shared_as_routed: Whether shared experts are appended as extra routed | ||
| 40 | + slots in the output. | ||
| 41 | + num_shared_experts: Number of shared experts. | ||
| 42 | + routed_scaling_factor: Multiplicative scaling applied to normalised weights. | ||
| 43 | + ep_rank: Expert-parallelism rank of this process. | ||
| 44 | + scoring_func: One of ``'sigmoid'``, ``'sqrtsoftplus'``, ``'softmax'``. | ||
| 45 | + mask: Boolean mask, shape ``(num_tokens,)``. ``True`` → route the token, | ||
| 46 | + ``False`` → fill outputs with ``-1`` / ``0``. | ||
| 47 | + fix_routing_mask: Boolean mask, shape ``(num_tokens,)``. When ``True`` for | ||
| 48 | + a token, use the indices already stored in *unmapped_topk_idx* instead of | ||
| 49 | + running the selection algorithm. | ||
| 50 | + to_physical_map: Logical-to-physical expert map, shape | ||
| 51 | + ``(num_logical_experts, num_duplicate_experts + 1)``, ``int32``. | ||
| 52 | + logical_count: Number of active duplicates per logical expert, shape | ||
| 53 | + ``(num_logical_experts,)``, ``int32``. | ||
| 54 | + unmapped_topk_idx: Output tensor (updated in-place) for unmasked expert | ||
| 55 | + indices, shape ``(num_tokens, num_topk)``, ``int64``. | ||
| 56 | + image_bias: Optional expert bias terms for VL of shape | ||
| 57 | + ``(num_routed_experts,)``, ``float32``. | ||
| 58 | + image_token_mask: Optional boolean mask, shape ``(num_tokens,)``. | ||
| 59 | + ``True`` stands for image tokens. | ||
| 60 | + out: Optional tuple of pre-allocated (topk_idx, topk_weights) tensors. | ||
| 61 | + | ||
| 62 | + Returns: | ||
| 63 | + topk_idx: Selected physical expert indices, shape | ||
| 64 | + ``(num_tokens, num_topk + num_shared_experts)``, ``int64``. | ||
| 65 | + topk_weights: Normalised expert weights, same shape, ``float32``. | ||
| 66 | + """ | ||
| 67 | + num_tokens_full, num_routed_experts = logits.shape | ||
| 68 | + scoring = ScoringFunc.from_str(scoring_func) | ||
| 69 | + | ||
| 70 | + if not use_shared_as_routed: | ||
| 71 | + num_shared_experts = 0 | ||
| 72 | + | ||
| 73 | + num_physical_topk = num_topk + num_shared_experts | ||
| 74 | + num_logical_experts = num_routed_experts + num_shared_experts | ||
| 75 | + device = logits.device | ||
| 76 | + | ||
| 77 | + if out is None: | ||
| 78 | + topk_idx_out = torch.full((num_tokens_full, num_physical_topk), -1, dtype=torch.int64, device=device) | ||
| 79 | + topk_weights_out = torch.zeros((num_tokens_full, num_physical_topk), dtype=torch.float32, device=device) | ||
| 80 | + else: | ||
| 81 | + topk_idx_out, topk_weights_out = out | ||
| 82 | + topk_idx_out.fill_(-1) | ||
| 83 | + topk_weights_out.zero_() | ||
| 84 | + | ||
| 85 | + if num_tokens_full == 0: | ||
| 86 | + return topk_idx_out, topk_weights_out | ||
| 87 | + | ||
| 88 | + active = mask if mask is not None else torch.ones(num_tokens_full, dtype=torch.bool, device=device) | ||
| 89 | + active_indices = active.nonzero(as_tuple=False).squeeze(1) | ||
| 90 | + num_tokens = active_indices.numel() | ||
| 91 | + | ||
| 92 | + if num_tokens == 0: | ||
| 93 | + if unmapped_topk_idx is not None: | ||
| 94 | + unmapped_topk_idx[~active] = -1 | ||
| 95 | + return topk_idx_out, topk_weights_out | ||
| 96 | + | ||
| 97 | + logits_a = logits[active_indices] | ||
| 98 | + if bias is None: | ||
| 99 | + bias = torch.zeros(num_routed_experts, dtype=torch.float32, device=device) | ||
| 100 | + | ||
| 101 | + # 1. Apply scoring function | ||
| 102 | + if scoring == ScoringFunc.SIGMOID: | ||
| 103 | + scores_wo_bias = torch.sigmoid(logits_a) | ||
| 104 | + elif scoring == ScoringFunc.SQRTSOFTPLUS: | ||
| 105 | + scores_wo_bias = F.softplus(logits_a).sqrt() | ||
| 106 | + elif scoring == ScoringFunc.SOFTMAX: | ||
| 107 | + scores_wo_bias = torch.softmax(logits_a, dim=-1) | ||
| 108 | + else: # Identity | ||
| 109 | + scores_wo_bias = logits_a | ||
| 110 | + | ||
| 111 | + # 2. Biased scores for ranking (softmax uses raw logits + bias) | ||
| 112 | + # Use image_bias for image tokens, regular bias otherwise | ||
| 113 | + if image_token_mask is not None: | ||
| 114 | + image_token_mask_a = image_token_mask[active_indices] | ||
| 115 | + bias_for_ranking = torch.where(image_token_mask_a.unsqueeze(-1), image_bias.unsqueeze(0), bias.unsqueeze(0)) | ||
| 116 | + else: | ||
| 117 | + bias_for_ranking = bias.unsqueeze(0) | ||
| 118 | + | ||
| 119 | + scores_biased = ( | ||
| 120 | + (logits_a + bias_for_ranking) if scoring == ScoringFunc.SOFTMAX else (scores_wo_bias + bias_for_ranking) | ||
| 121 | + ) | ||
| 122 | + | ||
| 123 | + # 3. Split tokens into normal routing and fix_routing | ||
| 124 | + fix_mask = torch.zeros(num_tokens, dtype=torch.bool, device=device) | ||
| 125 | + if fix_routing_mask is not None and unmapped_topk_idx is not None: | ||
| 126 | + fix_mask = fix_routing_mask[active_indices] | ||
| 127 | + | ||
| 128 | + topk_idx_local = torch.full((num_tokens, num_topk), -1, dtype=torch.int64, device=device) | ||
| 129 | + topk_score_local = torch.zeros((num_tokens, num_topk), dtype=torch.float32, device=device) | ||
| 130 | + | ||
| 131 | + # 4. Normal routing: select top-k experts | ||
| 132 | + normal_mask = ~fix_mask | ||
| 133 | + if normal_mask.any(): | ||
| 134 | + normal_indices = normal_mask.nonzero(as_tuple=False).squeeze(1) | ||
| 135 | + sb = scores_biased[normal_indices] | ||
| 136 | + | ||
| 137 | + selected = stable_topk(sb, num_topk) | ||
| 138 | + topk_idx_local[normal_indices] = selected | ||
| 139 | + topk_score_local[normal_indices] = scores_wo_bias[normal_indices].gather(1, selected) | ||
| 140 | + | ||
| 141 | + # 5. Fix routing: use pre-stored indices | ||
| 142 | + if fix_mask.any() and unmapped_topk_idx is not None: | ||
| 143 | + fix_indices = fix_mask.nonzero(as_tuple=False).squeeze(1) | ||
| 144 | + pre_idx = unmapped_topk_idx[active_indices[fix_indices]] | ||
| 145 | + topk_idx_local[fix_indices] = pre_idx | ||
| 146 | + topk_score_local[fix_indices] = scores_wo_bias[fix_indices].gather(1, pre_idx.clamp(min=0)) | ||
| 147 | + | ||
| 148 | + # 6. Write unmapped_topk_idx | ||
| 149 | + if unmapped_topk_idx is not None: | ||
| 150 | + unmapped_topk_idx[active_indices] = topk_idx_local | ||
| 151 | + if mask is not None: | ||
| 152 | + unmapped_topk_idx[~active] = -1 | ||
| 153 | + | ||
| 154 | + # 7. Normalise weights (top-sum normalisation) | ||
| 155 | + # NOTE: Align with moe_topk_gate kernel implementation, which adds 1e-20 instead of | ||
| 156 | + # clamping. With the identity scoring function the top-k sum can be negative, and | ||
| 157 | + # clamping it to 1e-20 would blow up the weights. | ||
| 158 | + topk_sum = topk_score_local.sum(dim=-1, keepdim=True) + 1e-20 | ||
| 159 | + topk_weights_routed = topk_score_local / topk_sum * routed_scaling_factor | ||
| 160 | + | ||
| 161 | + # 8. Append shared-expert slots | ||
| 162 | + if num_shared_experts > 0: | ||
| 163 | + shared_idx = torch.arange(num_routed_experts, num_logical_experts, dtype=torch.int64, device=device) | ||
| 164 | + topk_idx_all = torch.cat([topk_idx_local, shared_idx.expand(num_tokens, -1)], dim=1) | ||
| 165 | + topk_weights_all = torch.cat( | ||
| 166 | + [ | ||
| 167 | + topk_weights_routed, | ||
| 168 | + torch.ones((num_tokens, num_shared_experts), dtype=torch.float32, device=device), | ||
| 169 | + ], | ||
| 170 | + dim=1, | ||
| 171 | + ) | ||
| 172 | + else: | ||
| 173 | + topk_idx_all, topk_weights_all = topk_idx_local, topk_weights_routed | ||
| 174 | + | ||
| 175 | + # 9. Map logical → physical experts | ||
| 176 | + if to_physical_map is not None and logical_count is not None: | ||
| 177 | + physical_map_flat = to_physical_map.reshape(-1) | ||
| 178 | + map_width = to_physical_map.size(1) | ||
| 179 | + global_idx = active_indices.to(torch.int64) | ||
| 180 | + for lane in range(num_physical_topk): | ||
| 181 | + logical = topk_idx_all[:, lane] | ||
| 182 | + valid = logical >= 0 | ||
| 183 | + logical_safe = logical.clamp(min=0) | ||
| 184 | + count = torch.index_select(logical_count, 0, logical_safe).to(torch.int64) | ||
| 185 | + dup_idx = (ep_rank + global_idx * 23333) % count | ||
| 186 | + flat_idx = logical_safe * map_width + dup_idx | ||
| 187 | + physical = torch.index_select(physical_map_flat, 0, flat_idx).to(torch.int64) | ||
| 188 | + topk_idx_all[:, lane] = torch.where(valid, physical, logical) | ||
| 189 | + | ||
| 190 | + # 10. Write outputs | ||
| 191 | + topk_idx_out[active_indices] = topk_idx_all | ||
| 192 | + topk_weights_out[active_indices] = topk_weights_all | ||
| 193 | + | ||
| 194 | + return topk_idx_out, topk_weights_out | ||


这里只是举例,不是所有的算子都在这里列一下,前面已经有Engram举例了,所以这里的moe就不用加了。