Download code/models/common/tests/modules/moe/test_generalized_moe_gate.py from tt-hous/clef: direct link, hf CLI and curl.
- Browser
- Download file 26.9 kB
-
https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/modules/moe/test_generalized_moe_gate.py
- Command line
-
hf download hf://tt-hous/clef/code/models/common/tests/modules/moe/test_generalized_moe_gate.py
-
curl -L -o test_generalized_moe_gate.py https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/modules/moe/test_generalized_moe_gate.py
26.9 kB
| # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc. | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """Standalone unit test for the C++ op ``ttnn.experimental.deepseek.moe.generalized_moe_gate``. | |
| Exercises the device op directly against inlined PyTorch references, so the op can be validated in isolation | |
| without running the full ``MoEGate`` module. Covers all three of its modes: | |
| - ungrouped global top-k, 256 experts (``test_generalized_moe_gate``, vs ``_generalized_golden``); | |
| - ungrouped global top-k, 512 experts via the 2-block combine (``test_generalized_moe_gate_512_global``); | |
| - DeepSeek grouped gate via ``grouped=True`` (``test_generalized_moe_gate_grouped``, vs ``TTMoEGate.grouped_golden``) — | |
| the path the standalone ``deepseek_moe_gate`` op used to own. | |
| Modeled on ``models/demos/deepseek_v3_b1/tests/unit_tests/test_deepseek_moe_gate.py``. | |
| """ | |
| import pytest | |
| import torch | |
| from loguru import logger | |
| import ttnn | |
| from models.common.modules.moe.tt_moe_gate import TTMoEGate | |
| def _generalized_golden( | |
| input_tensor, bias_tensor, eps=1e-20, scaling_factor=2.5, enable_sigmoid=False, topk=8, output_softmax=False | |
| ): | |
| """PyTorch reference for the *ungrouped* generalized MoE gate: rank by the bias-corrected score, take | |
| the global top-`topk`, gather the UNBIASED score at those experts, normalize (softmax-over-selected if | |
| output_softmax else linear), scale. ``input_tensor``/``bias_tensor``: [batch, n_group, group_size].""" | |
| batch = input_tensor.shape[0] | |
| scores = torch.sigmoid(input_tensor) if enable_sigmoid else input_tensor | |
| bias_scores = scores + bias_tensor | |
| _, topk_indices = torch.topk(bias_scores.reshape(batch, -1), topk, dim=-1, sorted=True) | |
| topk_scores = torch.gather(scores.reshape(batch, -1), dim=-1, index=topk_indices) | |
| if output_softmax: | |
| # Subtract the per-row max before exp: numerically stable and mathematically identical | |
| # (softmax is shift-invariant). Mirrors the in-kernel max-subtraction so this reference stays | |
| # valid for RAW router logits (score_func="softmax"), not just inputs squashed to [0, 1]. | |
| topk_scores = topk_scores - topk_scores.max(dim=-1, keepdim=True).values | |
| weights = torch.exp(topk_scores) | |
| else: | |
| weights = topk_scores | |
| return weights / (torch.sum(weights, dim=-1, keepdim=True) + eps) * scaling_factor, topk_indices | |
| # The DeepSeek *grouped* gate reference (8 groups × 32 → top-2-sum → top-4 groups → top-8) lives on the | |
| # shared module as ``TTMoEGate.grouped_golden`` — the SINGLE source of truth, reused by the grouped test below. | |
| # logit_scale only matters on the raw-logit softmax path (see input gen): 1.0 = small/realistic regime, | |
| # 100.0 = past the bf16 exp ceiling (overflow stress). Other paths ignore it and run once (scale 1.0). | |
| def test_generalized_moe_gate(device, batch_size, enable_sigmoid, seed, topk, output_softmax, logit_scale): | |
| """Test the generalized MoE gate C++ op on a 32x32 tile against the golden reference (top-`topk`, | |
| linear-normalize or softmax-over-selected).""" | |
| raw_logit_softmax = output_softmax and not enable_sigmoid # the only path logit_scale affects | |
| if logit_scale != 1.0 and not raw_logit_softmax: | |
| pytest.skip("logit_scale only varies the raw-logit softmax path") | |
| # Tensor dimensions — full 32x32 tile, logical 32x32 per shard. | |
| input_shape = (batch_size, 8, 32) | |
| reshaped_input_shape = (batch_size, 16, 16) | |
| input_shard_shape = (32, 32) | |
| input_tile = ttnn.Tile(input_shard_shape) | |
| output_shape = (batch_size, 1, 16) | |
| output_shard_shape = (32, 32) | |
| output_tile = ttnn.Tile(output_shard_shape) | |
| logger.info(f"Testing generalized MoE gate with input shape {input_shape}") | |
| # Create input PyTorch tensor with random values. | |
| torch.manual_seed(seed) | |
| torch_input = (2 * torch.rand(input_shape, dtype=torch.bfloat16)) - 1 # ~[-1, 1] | |
| if enable_sigmoid: | |
| pass # the op sigmoids internally -> scores land in [0, 1]; a raw [-1, 1] input is fine | |
| elif output_softmax: | |
| # SOFTMAX path (score_func="softmax", enable_sigmoid=False). logit_scale sweeps two regimes: | |
| # 1.0 -> sigmoid to [0, 1]: the original small-magnitude coverage (benign, well inside the exp | |
| # range; also confirms the max-subtraction does not regress this case). | |
| # 100 -> raw ~[-100, 100]: UNBOUNDED router logits past the bf16 exp ceiling (~88), which exercises | |
| # the in-kernel max-subtraction — without it exp() saturates to inf -> nan/zero weights. | |
| torch_input = torch.sigmoid(torch_input) if logit_scale == 1.0 else torch_input * logit_scale | |
| else: | |
| # LINEAR-renorm path: keep scores in [0, 1] so the (Σ + eps) denominator stays well-conditioned. | |
| torch_input = torch.sigmoid(torch_input) | |
| torch_bias = (2 * torch.rand(input_shape, dtype=torch.bfloat16)) - 1 | |
| eps = 1e-20 | |
| scaling_factor = 2.5 | |
| # Reference output. (Only the golden indices are used — scores are validated tie-robustly below | |
| # against the device's OWN selection, not the golden's scores, so the golden scores are unused here.) | |
| _, top8_indices = _generalized_golden( | |
| torch_input, torch_bias, eps, scaling_factor, enable_sigmoid, topk, output_softmax | |
| ) | |
| grid = device.compute_with_storage_grid_size() | |
| core_grid = ttnn.num_cores_to_corerangeset( | |
| batch_size, | |
| ttnn.CoreCoord(grid.x, grid.y), | |
| row_wise=True, | |
| ) | |
| input_shard_spec = ttnn.ShardSpec( | |
| core_grid, | |
| input_shard_shape, | |
| ttnn.ShardOrientation.ROW_MAJOR, | |
| ) | |
| input_mem_config = ttnn.MemoryConfig(ttnn.TensorMemoryLayout.HEIGHT_SHARDED, ttnn.BufferType.L1, input_shard_spec) | |
| output_shard_spec = ttnn.ShardSpec( | |
| core_grid, | |
| output_shard_shape, | |
| ttnn.ShardOrientation.ROW_MAJOR, | |
| ) | |
| output_mem_config = ttnn.MemoryConfig(ttnn.TensorMemoryLayout.HEIGHT_SHARDED, ttnn.BufferType.L1, output_shard_spec) | |
| # Input values — sharded on a single core per batch. | |
| reshaped_input = torch.reshape(torch_input, reshaped_input_shape) | |
| ttnn_input = ttnn.from_torch( | |
| reshaped_input, | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=input_mem_config, | |
| tile=input_tile, | |
| ) | |
| # Bias is transposed before upload (the kernel expects the transposed layout). | |
| reshaped_bias = torch.transpose(torch.reshape(torch_bias, reshaped_input_shape), -2, -1) | |
| ttnn_bias = ttnn.from_torch( | |
| reshaped_bias, | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=input_mem_config, | |
| tile=input_tile, | |
| ) | |
| # Transposed routing indices: 0..255 laid out as (16,16) then transposed. | |
| torch_input_indices = torch.arange(reshaped_input_shape[1] * reshaped_input_shape[2], dtype=torch.int32) | |
| torch_input_indices = torch_input_indices.unsqueeze(0).expand(reshaped_input_shape[0], -1) | |
| torch_input_indices = torch_input_indices.reshape(reshaped_input_shape) | |
| torch_input_indices = torch.transpose(torch_input_indices, -2, -1).to(torch.uint16) | |
| ttnn_input_indices = ttnn.from_torch( | |
| torch_input_indices, | |
| dtype=ttnn.uint16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=input_mem_config, | |
| tile=input_tile, | |
| ) | |
| # Preallocated output buffers (filled in place by the op). | |
| ttnn_output = ttnn.from_torch( | |
| torch.zeros(output_shape, dtype=torch.bfloat16), | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=output_mem_config, | |
| tile=output_tile, | |
| ) | |
| ttnn_output_indices = ttnn.from_torch( | |
| torch.zeros(output_shape, dtype=torch.uint16), | |
| dtype=ttnn.uint16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=output_mem_config, | |
| tile=output_tile, | |
| ) | |
| logger.info("Running generalized MoE gate operation...") | |
| ttnn_result, ttnn_result_indices = ttnn.experimental.deepseek.moe.generalized_moe_gate( | |
| ttnn_input, | |
| bias_tensor=ttnn_bias, | |
| input_indices_tensor=ttnn_input_indices, | |
| output_tensor=ttnn_output, | |
| output_indices_tensor=ttnn_output_indices, | |
| eps=eps, | |
| scaling_factor=scaling_factor, | |
| enable_sigmoid=enable_sigmoid, | |
| topk=topk, | |
| output_softmax=output_softmax, | |
| ) | |
| # Convert back to torch and keep the top-`topk` slots (ranks 0..topk-1 sit in the first topk cols; | |
| # the dropped ranks topk..7 are zeroed by the kernel). | |
| output_torch = ttnn.to_torch(ttnn_result)[:, 0, :topk] | |
| output_indices_torch = ttnn.to_torch(ttnn_result_indices)[:, 0, :topk] | |
| # The op does not guarantee a stable order across ties, so sort both by index | |
| # before comparing (same approach as the reference unit test). | |
| sorted_output_indices_torch, i = torch.sort(output_indices_torch, dim=-1) | |
| sorted_output_torch = torch.gather(output_torch, dim=-1, index=i) | |
| top8_indices = torch.sort(top8_indices, dim=-1).values | |
| # bf16 produces many equal bias-corrected values, so the exact top-8 *indices* are ambiguous at | |
| # the rank-8 cutoff (genuine ties — e.g. two experts with identical bf16 bias fight for the last | |
| # slot, and torch.topk vs the device break it differently). A strict index match is the wrong | |
| # check. Validate tie-robustly: | |
| # (1) the device's selected experts form a VALID top-8 by the bias-corrected ranking key | |
| # (same sorted key multiset as the golden), and | |
| # (2) the normalized scores are self-consistent with the device's own selection. | |
| ranking = torch.sigmoid(torch_input) if enable_sigmoid else torch_input | |
| bias_key = (ranking + torch_bias).reshape(batch_size, -1).float() | |
| raw_scores = ranking.reshape(batch_size, -1).float() | |
| dev_idx = sorted_output_indices_torch.long() | |
| gold_idx = top8_indices.long() | |
| logger.info(f"dev_idx=\n{dev_idx}\ngold_idx=\n{gold_idx}") | |
| assert dev_idx.min() >= 0 and dev_idx.max() < 256, f"device produced out-of-range expert id:\n{dev_idx}" | |
| dev_key = torch.gather(bias_key, dim=-1, index=dev_idx).sort(dim=-1).values | |
| gold_key = torch.gather(bias_key, dim=-1, index=gold_idx).sort(dim=-1).values | |
| # bf16 ranks by a coarse key whose cell width scales with magnitude (ULP ≈ 2^-8·|key|): at ±100 it is | |
| # ~0.5, so experts whose float keys differ by up to ~0.5 round to the SAME bf16 key — genuinely tied to | |
| # the device, which may break the tie differently than the float32 golden. So scale the cutoff tolerance | |
| # with the logit magnitude: tight 1e-2 at the small/[0,1] scales, ~1.0 at ×100. A real mis-selection is | |
| # off by ≫ that and still fails. (Non-raw paths run only at scale 1.0, so they keep the tight 1e-2.) | |
| key_atol = 1e-2 * max(1.0, logit_scale) | |
| assert torch.allclose(dev_key, gold_key, atol=key_atol), ( | |
| f"Device selection is not a valid top-8 by bias key.\n dev_idx={dev_idx}\n gold_idx={gold_idx}" | |
| f"\n dev_key={dev_key}\n gold_key={gold_key}" | |
| ) | |
| dev_sel = torch.gather(raw_scores, dim=-1, index=dev_idx) | |
| # Consistency check vs the device's OWN selection: softmax-over-selected when output_softmax, else linear. | |
| if output_softmax: | |
| # Max-subtract before exp (matches the kernel; stable for raw logits, identical for [0, 1]). | |
| weights = torch.exp(dev_sel - dev_sel.max(dim=-1, keepdim=True).values) | |
| else: | |
| weights = dev_sel | |
| expected_norm = weights / (weights.sum(dim=-1, keepdim=True) + eps) * scaling_factor | |
| assert torch.allclose( | |
| sorted_output_torch.float(), expected_norm, atol=1e-2, rtol=1e-4 | |
| ), "Normalized scores are not consistent with the device's own top-8 selection" | |
| # logit_scale only matters on the raw-logit softmax path: 1.0 = small/realistic, 100.0 = overflow stress. | |
| def test_generalized_moe_gate_512_global(device, batch_size, enable_sigmoid, seed, topk, output_softmax, logit_scale): | |
| """512-expert true GLOBAL top-8 (A2 combine). Each of the 2 blocks produces a re-mergeable top-8 | |
| RUN (idx made global via +b*256), stashed to L1; the combine places run0 at {0,2} and run1 at | |
| {4,6} and finalizes -> the global top-8 over all 512 experts (indices 0-511). GMG_DIAG_BLOCK must | |
| be UNSET in the kernel. Input layout = slice (each 256-block -> face0 of its own 32x32 tile).""" | |
| raw_logit_softmax = output_softmax and not enable_sigmoid # the only path logit_scale affects | |
| if logit_scale != 1.0 and not raw_logit_softmax: | |
| pytest.skip("logit_scale only varies the raw-logit softmax path") | |
| num_experts = 512 | |
| num_blocks = num_experts // 256 | |
| eps, scaling_factor = 1e-20, 2.5 | |
| tile = ttnn.Tile((32, 32)) | |
| torch.manual_seed(seed) | |
| torch_input = (2 * torch.rand((batch_size, num_experts), dtype=torch.bfloat16)) - 1 # ~[-1, 1] | |
| if enable_sigmoid: | |
| pass # the op sigmoids internally -> scores land in [0, 1]; a raw [-1, 1] input is fine | |
| elif output_softmax: | |
| # SOFTMAX path (score_func="softmax", enable_sigmoid=False), across the 512 combine. logit_scale: | |
| # 1.0 -> sigmoid to [0, 1]: original small-magnitude coverage (also confirms max-sub doesn't regress). | |
| # 100 -> raw ~[-100, 100]: UNBOUNDED logits past the bf16 exp ceiling (~88) -> exercises max-sub. | |
| torch_input = torch.sigmoid(torch_input) if logit_scale == 1.0 else torch_input * logit_scale | |
| else: | |
| # LINEAR-renorm path: keep scores in [0, 1] so the (Σ + eps) denominator stays well-conditioned. | |
| torch_input = torch.sigmoid(torch_input) | |
| torch_bias = (2 * torch.rand((batch_size, num_experts), dtype=torch.bfloat16)) - 1 | |
| # Golden: flatten (batch, 512) -> true global top-`topk` (indices 0-511). Only the golden INDICES are | |
| # used (selection check below); scores are validated against the device's OWN selection, not the | |
| # golden's, because a bf16 tie at the cutoff can pick different-but-valid experts (see the score check). | |
| _, gold_idx = _generalized_golden( | |
| torch_input, torch_bias, eps, scaling_factor, enable_sigmoid, topk, output_softmax | |
| ) | |
| scores_all = (torch.sigmoid(torch_input) if enable_sigmoid else torch_input).float() | |
| bias_key = scores_all + torch_bias.float() # bias-corrected ranking key, (batch, 512) | |
| logits_blocks = torch_input.reshape(batch_size, num_blocks, 16, 16) | |
| # bias uploaded transposed within each (16,16) block (kernel expects the transposed layout). | |
| bias_blocks = torch.transpose(torch_bias.reshape(batch_size, num_blocks, 16, 16), -2, -1).contiguous() | |
| grid = device.compute_with_storage_grid_size() | |
| core_grid = ttnn.num_cores_to_corerangeset(batch_size, ttnn.CoreCoord(grid.x, grid.y), row_wise=True) | |
| def mem(shard): | |
| return ttnn.MemoryConfig( | |
| ttnn.TensorMemoryLayout.HEIGHT_SHARDED, | |
| ttnn.BufferType.L1, | |
| ttnn.ShardSpec(core_grid, shard, ttnn.ShardOrientation.ROW_MAJOR), | |
| ) | |
| multi, one = (num_blocks * 32, 32), (32, 32) | |
| ttnn_input = ttnn.from_torch( | |
| logits_blocks, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, memory_config=mem(multi), tile=tile | |
| ) | |
| ttnn_bias = ttnn.from_torch( | |
| bias_blocks, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, memory_config=mem(multi), tile=tile | |
| ) | |
| # input_indices: one tile per block, holding that block's GLOBAL expert ids (block b = arange + b*256), | |
| # transposed per block (kernel expects the transposed layout). The pipeline tracks global ids directly. | |
| ar = torch.arange(256, dtype=torch.int32).reshape(1, 1, 16, 16) | |
| offs = (torch.arange(num_blocks, dtype=torch.int32) * 256).reshape(1, num_blocks, 1, 1) | |
| idx_blocks = torch.transpose(ar + offs, -2, -1).contiguous().to(torch.uint16) # (1, num_blocks, 16, 16) | |
| # The ids are batch-independent (arange + block offset), but the shard grid has one core per batch row, | |
| # so replicate to batch_size — otherwise rows >0 get an unfilled (zero) index shard and route on id 0. | |
| idx_blocks = idx_blocks.expand(batch_size, -1, -1, -1).contiguous() # (batch_size, num_blocks, 16, 16) | |
| ttnn_input_indices = ttnn.from_torch( | |
| idx_blocks, dtype=ttnn.uint16, layout=ttnn.TILE_LAYOUT, device=device, memory_config=mem(multi), tile=tile | |
| ) | |
| out_shape = (batch_size, 1, 16) | |
| ttnn_output = ttnn.from_torch( | |
| torch.zeros(out_shape, dtype=torch.bfloat16), | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=mem(one), | |
| tile=tile, | |
| ) | |
| ttnn_output_indices = ttnn.from_torch( | |
| torch.zeros(out_shape, dtype=torch.uint16), | |
| dtype=ttnn.uint16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=mem(one), | |
| tile=tile, | |
| ) | |
| res_scores, res_idx = ttnn.experimental.deepseek.moe.generalized_moe_gate( | |
| ttnn_input, | |
| bias_tensor=ttnn_bias, | |
| input_indices_tensor=ttnn_input_indices, | |
| output_tensor=ttnn_output, | |
| output_indices_tensor=ttnn_output_indices, | |
| eps=eps, | |
| scaling_factor=scaling_factor, | |
| enable_sigmoid=enable_sigmoid, | |
| topk=topk, | |
| output_softmax=output_softmax, | |
| ) | |
| dev_idx = ttnn.to_torch(res_idx)[:, 0, :topk].to(torch.int64) | |
| dev_scores = ttnn.to_torch(res_scores)[:, 0, :topk].float() | |
| logger.info(f"512 global (topk={topk}): dev_idx={dev_idx} gold_idx={gold_idx}") | |
| # Indices: dev must be a valid GLOBAL top-`topk` (tie-robust: compare the gathered bias-keys, sorted). | |
| dev_key = torch.gather(bias_key, -1, dev_idx).sort(-1).values | |
| gold_key = torch.gather(bias_key, -1, gold_idx.to(torch.int64)).sort(-1).values | |
| # bf16 ranks by a coarse key whose cell width scales with magnitude (ULP ≈ 2^-8·|key|): at ±100 it is | |
| # ~0.5, so experts whose float keys differ by up to ~0.5 round to the SAME bf16 key — genuinely tied to | |
| # the device, which may break the tie differently than the float32 golden. Scale the cutoff tolerance | |
| # with the logit magnitude: tight 1e-2 at the small/[0,1] scales, ~1.0 at ×100. A real mis-selection is | |
| # off by ≫ that and still fails. (Non-raw paths run only at scale 1.0, so they keep the tight 1e-2.) | |
| key_atol = 1e-2 * max(1.0, logit_scale) | |
| assert torch.allclose(dev_key, gold_key, atol=key_atol), ( | |
| f"512 global not a valid top-{topk}.\n dev_idx={dev_idx}\n gold_idx={gold_idx}\n" | |
| f" dev_key={dev_key}\n gold_key={gold_key}" | |
| ) | |
| # Scores: validate against the device's OWN selection (selection-agnostic, like the 256 test). At ±100 | |
| # the device and golden may break a bf16 tie toward DIFFERENT (equally valid) experts, so comparing to | |
| # the golden's scores is wrong; recompute the expected softmax/linear weights over the device's selected | |
| # raw scores instead. The selection check above already confirmed those experts are a valid top-`topk`. | |
| dev_sel = torch.gather(scores_all, -1, dev_idx) | |
| if output_softmax: | |
| # Max-subtract before exp (matches the kernel; stable for raw logits, identical for [0, 1]). | |
| w = torch.exp(dev_sel - dev_sel.max(-1, keepdim=True).values) | |
| else: | |
| w = dev_sel | |
| expected = w / (w.sum(-1, keepdim=True) + eps) * scaling_factor | |
| # Position-aligned, NOT sorted independently: dev_scores[i] and expected[i] both correspond to expert | |
| # dev_idx[i], so they must match elementwise. Sorting each side separately would only check the weight | |
| # multiset and would pass even if the kernel paired the right weights with the wrong ids — a real MoE | |
| # bug, since combine applies weight[i] to expert dev_idx[i]. | |
| assert torch.allclose( | |
| dev_scores, expected, atol=2e-2 | |
| ), f"512 normalized scores not consistent with device selection.\n dev={dev_scores}\n expected={expected}" | |
| def test_generalized_moe_gate_grouped(device, batch_size, enable_sigmoid, seed): | |
| """DeepSeek GROUPED gate via ``generalized_moe_gate(grouped=True)``: 256 experts = 8 groups × 32 -> | |
| top-2-sum per group -> top-4 groups -> top-8, linear renorm + scale. Confirms the unified op's grouped | |
| path (ungrouped_top8=false via the moe_gate_ungrouped_top8 CT arg) matches the grouped golden — the path the standalone | |
| ``deepseek_moe_gate`` op used to own. grouped fixes top-8 + linear renorm, so topk / output_softmax are | |
| not swept (the op rejects other values in that mode).""" | |
| eps, scaling_factor = 1e-20, 2.5 | |
| input_shape = (batch_size, 8, 32) | |
| reshaped_input_shape = (batch_size, 16, 16) | |
| shard = (32, 32) | |
| tile = ttnn.Tile(shard) | |
| out_shape = (batch_size, 1, 16) | |
| torch.manual_seed(seed) | |
| torch_input = (2 * torch.rand(input_shape, dtype=torch.bfloat16)) - 1 # ~[-1, 1] | |
| if not enable_sigmoid: | |
| # No in-op sigmoid: keep scores in [0, 1] so the (Σ + eps) linear-renorm denominator is well-conditioned. | |
| torch_input = torch.sigmoid(torch_input) | |
| torch_bias = (2 * torch.rand(input_shape, dtype=torch.bfloat16)) - 1 | |
| # Golden INDICES only — scores are validated against the device's OWN selection below (tie-robust). | |
| _, gold_idx = TTMoEGate.grouped_golden( | |
| torch_input, torch_bias, eps=eps, scaling_factor=scaling_factor, enable_sigmoid=enable_sigmoid | |
| ) | |
| grid = device.compute_with_storage_grid_size() | |
| core_grid = ttnn.num_cores_to_corerangeset(batch_size, ttnn.CoreCoord(grid.x, grid.y), row_wise=True) | |
| mem = ttnn.MemoryConfig( | |
| ttnn.TensorMemoryLayout.HEIGHT_SHARDED, | |
| ttnn.BufferType.L1, | |
| ttnn.ShardSpec(core_grid, shard, ttnn.ShardOrientation.ROW_MAJOR), | |
| ) | |
| # Same single-256-block device layout as the ungrouped 256 test — only grouped=True differs in the call. | |
| ttnn_input = ttnn.from_torch( | |
| torch.reshape(torch_input, reshaped_input_shape), | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=mem, | |
| tile=tile, | |
| ) | |
| # Bias is transposed within each (16,16) block before upload (the kernel expects the transposed layout). | |
| reshaped_bias = torch.transpose(torch.reshape(torch_bias, reshaped_input_shape), -2, -1) | |
| ttnn_bias = ttnn.from_torch( | |
| reshaped_bias, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, memory_config=mem, tile=tile | |
| ) | |
| # Transposed routing indices: 0..255 laid out as (16,16) then transposed. | |
| torch_input_indices = torch.arange(reshaped_input_shape[1] * reshaped_input_shape[2], dtype=torch.int32) | |
| torch_input_indices = torch_input_indices.unsqueeze(0).expand(reshaped_input_shape[0], -1) | |
| torch_input_indices = torch_input_indices.reshape(reshaped_input_shape) | |
| torch_input_indices = torch.transpose(torch_input_indices, -2, -1).to(torch.uint16) | |
| ttnn_input_indices = ttnn.from_torch( | |
| torch_input_indices, dtype=ttnn.uint16, layout=ttnn.TILE_LAYOUT, device=device, memory_config=mem, tile=tile | |
| ) | |
| ttnn_output = ttnn.from_torch( | |
| torch.zeros(out_shape, dtype=torch.bfloat16), | |
| dtype=ttnn.bfloat16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=mem, | |
| tile=tile, | |
| ) | |
| ttnn_output_indices = ttnn.from_torch( | |
| torch.zeros(out_shape, dtype=torch.uint16), | |
| dtype=ttnn.uint16, | |
| layout=ttnn.TILE_LAYOUT, | |
| device=device, | |
| memory_config=mem, | |
| tile=tile, | |
| ) | |
| logger.info("Running generalized MoE gate (grouped=True) ...") | |
| res_scores, res_idx = ttnn.experimental.deepseek.moe.generalized_moe_gate( | |
| ttnn_input, | |
| bias_tensor=ttnn_bias, | |
| input_indices_tensor=ttnn_input_indices, | |
| output_tensor=ttnn_output, | |
| output_indices_tensor=ttnn_output_indices, | |
| eps=eps, | |
| scaling_factor=scaling_factor, | |
| enable_sigmoid=enable_sigmoid, | |
| topk=8, | |
| output_softmax=False, | |
| grouped=True, | |
| ) | |
| output_torch = ttnn.to_torch(res_scores)[:, 0, :8] | |
| output_indices_torch = ttnn.to_torch(res_idx)[:, 0, :8] | |
| # Sort by index so the device's (tie-arbitrary) order lines up with the golden for the score check. | |
| sorted_idx, i = torch.sort(output_indices_torch, dim=-1) | |
| sorted_scores = torch.gather(output_torch, dim=-1, index=i) | |
| ranking = torch.sigmoid(torch_input) if enable_sigmoid else torch_input | |
| bias_key = (ranking + torch_bias).reshape(batch_size, -1).float() # bias-corrected ranking key (256) | |
| raw_scores = ranking.reshape(batch_size, -1).float() # UNBIASED scores | |
| dev_idx = sorted_idx.long() | |
| gold_idx = torch.sort(gold_idx, dim=-1).values.long() | |
| logger.info(f"grouped: dev_idx=\n{dev_idx}\ngold_idx=\n{gold_idx}") | |
| assert dev_idx.min() >= 0 and dev_idx.max() < 256, f"device produced out-of-range expert id:\n{dev_idx}" | |
| # (1) Selection: the device's chosen experts match the GROUPED golden's by bias-corrected key (sorted | |
| # multiset) — NOT a global top-8, the grouped golden's own selection. Tie-robust: a bf16 tie at the | |
| # group / rank-8 boundary may swap near-equal-key experts, so compare key VALUES not index positions; | |
| # a real grouping/wiring bug shifts a key by >> the tolerance. | |
| dev_key = torch.gather(bias_key, -1, dev_idx).sort(-1).values | |
| gold_key = torch.gather(bias_key, -1, gold_idx).sort(-1).values | |
| assert torch.allclose(dev_key, gold_key, atol=1e-2), ( | |
| f"grouped selection not consistent with the grouped golden.\n dev_idx={dev_idx}\n gold_idx={gold_idx}\n" | |
| f" dev_key={dev_key}\n gold_key={gold_key}" | |
| ) | |
| # (2) Scores: self-consistent with the device's OWN selection — linear renorm of the UNBIASED scores at | |
| # the experts the device picked, scaled. Position-aligned (both indexed by the sorted dev ids). | |
| dev_sel = torch.gather(raw_scores, -1, dev_idx) | |
| expected = dev_sel / (dev_sel.sum(-1, keepdim=True) + eps) * scaling_factor | |
| assert torch.allclose( | |
| sorted_scores.float(), expected, atol=1e-2, rtol=1e-4 | |
| ), f"grouped normalized scores not consistent with device selection.\n dev={sorted_scores}\n expected={expected}" | |