Skip to content

perf(dsv4/moe): predicate empty routed-expert stages - #809

Open
wangqin1723-max wants to merge 4 commits into
hw-native-sys:mainfrom
wangqin1723-max:pr-738-from-main
Open

perf(dsv4/moe): predicate empty routed-expert stages#809
wangqin1723-max wants to merge 4 commits into
hw-native-sys:mainfrom
wangqin1723-max:pr-738-from-main

Conversation

@wangqin1723-max

Copy link
Copy Markdown
Contributor

Summary

  • Predicate each routed-expert SPMD stage (gate / up / act / quant / w2) on recv_expert_count[local, 0] > 0 so experts with zero tokens skip all compute.
  • Carry per-expert TaskIDs (gate_tids / up_tids / act_tid / quant_tid) across the split compute passes and wire them as explicit deps=[count_tid, gather_tid, ...] edges, so predicated tasks chain correctly while auto dependency tracking still covers tensor reads/writes.
  • dispatch() returns its _meta_tid / _gather_tid and threads them into expert_routed as the count/gather producers.
  • Flatten recv_x to a 2D ND view (recv_x_flat) to stop PTOAS inferring NZ on the 3-D view when RECV_MAX == 32 (EP=4), which conflicted with the row-major matmul input layout.
  • Switch exp_h_q from pl.at(CORE_GROUP) to pl.spmd(1) for a uniform predicated scope; lower n_routed_experts 256 -> 128 (16/rank) so more experts hit the empty path.

This branch also carries the routed-expert stage-hoisting from #738 (launch each stage once per expert; read the routed row count inside each stage), on top of which the predicate work is layered.

Related Issues

Stacks on #738.

zhangqi-chen and others added 4 commits July 20, 2026 21:23
Each routed expert stage (gate mm, up mm, SwiGLU act, h requant, w2 mm)
used to sit inside a `pl.parallel(n_tiles)` row-tile loop, so every stage
was launched once per row tile. Push the tile loop inside each stage
instead: a stage now launches once per local expert and iterates its
valid row tiles internally, cutting the runtime task count.

- Widen the per-expert intermediates (gate/up/h) from [RECV_TILE, ...]
  to [RECV_MAX, ...] so data flows across stages.
- Promote y_i32 and h_tile_scale_dq to flat expert-major
  [N_LOCAL_EXPERTS * RECV_MAX, ...] tensors, since they cross loops.
- Split the w2 dequant + routing-weight epilogue into its own expert
  loop. recv_y's nz layout forces the writeback through pl.assemble,
  which is frame-level only, so that loop keeps a frame-level tile loop
  and assembles each tile right after the spmd produces it.
The per-expert valid row count (recv_expert_count) was read at the
pl.parallel frame level, so a single AICPU scalar task fed every routed
compute stage. Move the pl.read into each stage's own scope (gate mm, up
mm, SwiGLU act, h requant, w2 mm) so the count scalar lives on the
stage's cores. moe ep2 PASS on a2a3.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d20e68b-a7f5-4ced-89a8-c85517b896ad

📥 Commits

Reviewing files that changed from the base of the PR and between 0999dba and f46cfc8.

📒 Files selected for processing (4)
  • models/deepseek/v4/config.py
  • models/deepseek/v4/expert_routed.py
  • models/deepseek/v4/gate.py
  • models/deepseek/v4/moe.py

📝 Walkthrough

Walkthrough

DeepSeek-V4 FLASH now routes through 128 experts. Dispatch task handles flow into expert_routed, whose gate/up, activation, quantization, and W2 stages use explicit SPMD dependencies and flattened indexing.

Changes

DeepSeek-V4 routed expert execution

Layer / File(s) Summary
Dispatch dependency propagation
models/deepseek/v4/moe.py, models/deepseek/v4/config.py, models/deepseek/v4/gate.py
dispatch returns task handles that moe passes into expert_routed; FLASH routing configuration and related sizing comments now describe the 128/16-expert setup.
Routed expert SPMD pipeline
models/deepseek/v4/expert_routed.py
Gate/up accumulation, activation, quantization, and W2 stages use explicit predicates, task dependencies, and flattened tensor offsets.
Routing contract and harness updates
models/deepseek/v4/expert_routed.py
The test harness supplies invalid task IDs for the new expert_routed parameters.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant moe
  participant dispatch
  participant expert_routed
  participant SPMDStages
  moe->>dispatch: Produce count_tid and gather_tid
  dispatch-->>moe: Return task handles
  moe->>expert_routed: Pass dispatch dependencies
  expert_routed->>SPMDStages: Run gated, activation, quantization, and W2 stages
  SPMDStages-->>expert_routed: Store routed output accumulators
Loading

Possibly related PRs

Poem

A rabbit hops through experts bright,
With task IDs lined up just right.
Gate, quantize, then W2 flows,
Flattened paths beneath its toes—
One hundred twenty-eight in sight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: predicate-based optimization for routed-expert stages in DeepSeek-V4 MoE.
Description check ✅ Passed The description directly summarizes the routing predicate, task-ID wiring, layout fix, and config changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the DeepSeek-V4 MoE single-layer decode implementation, reducing the number of routed experts per rank from 32 to 16 and introducing explicit task-dependency tracking (using task IDs) to optimize SPMD execution and predication. The feedback suggests further optimizing the expert_routed kernel by carrying and saving the task ID for the exp_w2_mm stage (w2_tids), and hoisting the exp_w2_act stage outside the tile loop to reduce launch overhead, eliminate temporary tensors, and avoid unnecessary assembly operations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +79 to +80
gate_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)
up_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To support hoisting and predication of the exp_w2_act stage, we should carry its dependency task ID w2_tids across the passes, similar to gate_tids and up_tids.

Suggested change
gate_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)
up_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)
gate_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)
up_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)
w2_tids = pl.array.create(N_LOCAL_EXPERTS, pl.TASK_ID)

Comment on lines +239 to +264
with pl.spmd(
D // (W2_INNER * D_OUT_TILE),
name_hint="exp_w2_mm",
deps=[count_tid, quant_tid],
predicate=(recv_expert_count[local_e, 0] > 0),
) as _w2_tid:
w2_rows = pl.read(recv_expert_count, [local_e, 0])
w2_tiles = (w2_rows + RECV_TILE - 1) // RECV_TILE
wb_idx = pl.tile.get_block_idx()
d_base = wb_idx * (W2_INNER * D_OUT_TILE)
for w2_t in pl.range(w2_tiles):
w2_t0 = w2_t * RECV_TILE
for dg in pl.range(W2_INNER):
d0 = d_base + dg * D_OUT_TILE
y_acc = pl.create_tensor([1, RECV_TILE, D_OUT_TILE], dtype=pl.INT32)
for k0 in pl.pipeline(0, MOE_INTER, INTER_K, stage=2):
h_k = h_tile_i8[:, k0 : k0 + INTER_K]
h_k = h_tile_i8[w2_t0 : w2_t0 + RECV_TILE, k0 : k0 + INTER_K]
w2_k = routed_w2[local_e : local_e + 1, d0 : d0 + D_OUT_TILE, k0 : k0 + INTER_K]
if k0 == 0:
y_acc = pl.matmul(h_k, w2_k, b_trans=True, out_dtype=pl.INT32)
else:
y_acc = pl.matmul_acc(y_acc, h_k, w2_k, b_trans=True)
y_i32[:, d0 : d0 + D_OUT_TILE] = pl.reshape(y_acc, [RECV_TILE, D_OUT_TILE])

y_i32[
e_flat_base + w2_t0 : e_flat_base + w2_t0 + RECV_TILE,
d0 : d0 + D_OUT_TILE,
] = pl.reshape(y_acc, [RECV_TILE, D_OUT_TILE])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Save the task ID of exp_w2_mm into w2_tids so that the hoisted exp_w2_act stage can explicitly depend on it for correct chaining of predicated tasks.

        with pl.spmd(
            D // (W2_INNER * D_OUT_TILE),
            name_hint="exp_w2_mm",
            deps=[count_tid, quant_tid],
            predicate=(recv_expert_count[local_e, 0] > 0),
        ) as w2_tid:
            w2_rows = pl.read(recv_expert_count, [local_e, 0])
            w2_tiles = (w2_rows + RECV_TILE - 1) // RECV_TILE
            wb_idx = pl.tile.get_block_idx()
            d_base = wb_idx * (W2_INNER * D_OUT_TILE)
            for w2_t in pl.range(w2_tiles):
                w2_t0 = w2_t * RECV_TILE
                for dg in pl.range(W2_INNER):
                    d0 = d_base + dg * D_OUT_TILE
                    y_acc = pl.create_tensor([1, RECV_TILE, D_OUT_TILE], dtype=pl.INT32)
                    for k0 in pl.pipeline(0, MOE_INTER, INTER_K, stage=2):
                        h_k = h_tile_i8[w2_t0 : w2_t0 + RECV_TILE, k0 : k0 + INTER_K]
                        w2_k = routed_w2[local_e : local_e + 1, d0 : d0 + D_OUT_TILE, k0 : k0 + INTER_K]
                        if k0 == 0:
                            y_acc = pl.matmul(h_k, w2_k, b_trans=True, out_dtype=pl.INT32)
                        else: 
                            y_acc = pl.matmul_acc(y_acc, h_k, w2_k, b_trans=True)
                    y_i32[
                        e_flat_base + w2_t0 : e_flat_base + w2_t0 + RECV_TILE,
                        d0 : d0 + D_OUT_TILE,
                    ] = pl.reshape(y_acc, [RECV_TILE, D_OUT_TILE])
        w2_tids[local_e] = w2_tid
References
  1. In PyPTO on CANN/Ascend hardware, keeping a conditional check (e.g., if db == 0) inside a pl.pipeline loop can be preferred over peeling the first iteration. This allows the first chunk's load to overlap with the rest of the pipeline rather than running as an un-pipelined prologue, provided the compiler successfully pipelines the loop-index branch.

Comment on lines +266 to +297
for output_e in pl.parallel(N_LOCAL_EXPERTS):
output_rows = pl.read(recv_expert_count, [output_e, 0])
output_tiles = (output_rows + RECV_TILE - 1) // RECV_TILE
output_flat_base = output_e * RECV_MAX
for output_t in pl.range(output_tiles):
output_t0 = output_t * RECV_TILE
flat_output_t0 = output_flat_base + output_t0
recv_y_tile = pl.create_tensor([RECV_TILE, D], dtype=pl.BF16)
with pl.spmd(D // (W2_ACT_INNER * D_OUT_TILE_ACT), name_hint="exp_w2_act"):
db_idx = pl.tile.get_block_idx()
act_d_base = db_idx * (W2_ACT_INNER * D_OUT_TILE_ACT)
w_col_blk = pl.reshape(
recv_weights[local_e : local_e + 1, tt0 : tt0 + RECV_TILE],
recv_weights[output_e : output_e + 1, output_t0 : output_t0 + RECV_TILE],
[RECV_TILE, 1],
)
row_scale_blk = pl.mul(h_tile_scale_dq, w_col_blk)
row_scale_blk = pl.mul(
h_tile_scale_dq[flat_output_t0 : flat_output_t0 + RECV_TILE, :], w_col_blk
)
for dg in pl.pipeline(W2_ACT_INNER, stage=2):
act_d0 = act_d_base + dg * D_OUT_TILE_ACT
y_2d_i32 = y_i32[:, act_d0 : act_d0 + D_OUT_TILE_ACT]
w2_scale_chunk = routed_w2_scale[local_e : local_e + 1, act_d0 : act_d0 + D_OUT_TILE_ACT]
y_2d_i32 = y_i32[
flat_output_t0 : flat_output_t0 + RECV_TILE, act_d0 : act_d0 + D_OUT_TILE_ACT
]
w2_scale_chunk = routed_w2_scale[
output_e : output_e + 1, act_d0 : act_d0 + D_OUT_TILE_ACT
]
y_2d = pl.cast(y_2d_i32, target_type=pl.FP32, mode="none")
y_2d = pl.col_expand_mul(pl.row_expand_mul(y_2d, row_scale_blk), w2_scale_chunk)
recv_y_tile[:, act_d0 : act_d0 + D_OUT_TILE_ACT] = pl.cast(
y_2d, target_type=pl.BF16, mode="rint"
)
recv_y_flat = pl.assemble(recv_y_flat, recv_y_tile, [flat_tt0, 0])
recv_y_flat = pl.assemble(recv_y_flat, recv_y_tile, [flat_output_t0, 0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The exp_w2_act stage is currently launched inside the for output_t in pl.range(output_tiles): loop, which incurs significant launch overhead for every tile.

We can optimize this by hoisting exp_w2_act outside the loop (similar to the other stages) and looping inside the SPMD block. This also allows us to predicate exp_w2_act on recv_expert_count[output_e, 0] > 0 and write directly to recv_y_flat, completely eliminating the temporary recv_y_tile tensor and the pl.assemble call.

    for output_e in pl.parallel(N_LOCAL_EXPERTS):
        output_rows = pl.read(recv_expert_count, [output_e, 0])
        output_tiles = (output_rows + RECV_TILE - 1) // RECV_TILE
        output_flat_base = output_e * RECV_MAX
        with pl.spmd(
            D // (W2_ACT_INNER * D_OUT_TILE_ACT),
            name_hint="exp_w2_act",
            deps=[count_tid, w2_tids[output_e]],
            predicate=(recv_expert_count[output_e, 0] > 0),
        ):
            db_idx = pl.tile.get_block_idx()
            act_d_base = db_idx * (W2_ACT_INNER * D_OUT_TILE_ACT)
            for output_t in pl.range(output_tiles):
                output_t0 = output_t * RECV_TILE
                flat_output_t0 = output_flat_base + output_t0
                w_col_blk = pl.reshape(
                    recv_weights[output_e : output_e + 1, output_t0 : output_t0 + RECV_TILE],
                    [RECV_TILE, 1],
                )
                row_scale_blk = pl.mul(
                    h_tile_scale_dq[flat_output_t0 : flat_output_t0 + RECV_TILE, :], w_col_blk
                )
                for dg in pl.pipeline(W2_ACT_INNER, stage=2):
                    act_d0 = act_d_base + dg * D_OUT_TILE_ACT
                    y_2d_i32 = y_i32[
                        flat_output_t0 : flat_output_t0 + RECV_TILE, act_d0 : act_d0 + D_OUT_TILE_ACT
                    ]
                    w2_scale_chunk = routed_w2_scale[
                        output_e : output_e + 1, act_d0 : act_d0 + D_OUT_TILE_ACT
                    ]
                    y_2d = pl.cast(y_2d_i32, target_type=pl.FP32, mode="none")
                    y_2d = pl.col_expand_mul(pl.row_expand_mul(y_2d, row_scale_blk), w2_scale_chunk)
                    recv_y_flat[
                        flat_output_t0 : flat_output_t0 + RECV_TILE, act_d0 : act_d0 + D_OUT_TILE_ACT
                    ] = pl.cast(y_2d, target_type=pl.BF16, mode="rint")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants