Skip to content

MoE hot experts: one batched device call per layer - #38

Merged
gspark-etri merged 4 commits into
mainfrom
perf/35-batch-hot-experts
Oct 21, 2026
Merged

gspark-etri merged 4 commits into
mainfrom
perf/35-batch-hot-experts

Conversation

@gspark-etri

@gspark-etri gspark-etri commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Background

With hot experts on RNGD (#27), Qwen3-30B-A3B LoRA ran about 615 s/step against about 195 s/step host-only. Each hot expert was its own device call, with its own upload and download (#35).

Changes

  • npu_lora/moe_device.py: a layer's hot experts now run as one device call per direction.
    • The rows routed to hot experts are packed into [E_hot, bucket, H]. The bucket is the busiest hot expert's row count, rounded up to 8…1024.
    • experts_fwd / experts_bwd_fn are bmm over stacked weights [E_hot, 2F, H] / [E_hot, H, F]. Graphs compile once per (E_hot, H, F, bucket).
    • The stacked weights stay resident on the device per layer. They are re-uploaded only when that layer's hot set changes (replan) or after a full-FT update (refresh).
    • Full fine-tuning: the host stacks gate_up_proj[hot] carry the weight gradients back through autograd. LoRA passes no host stacks, which avoids a 300 MB copy per layer.
    • Cold experts stay on the host.
  • moe_stats() also reports weight_uploads / weight_upload_s.
  • Tests:
    • new test_hot_expert_packing: packing layout, and the batched graph equals the per-expert computation;
    • test_moe_hot_experts_on_device_match_host now also checks the call count: 3 per layer (forward, the forward that HostMLP.backward recomputes, backward), not 3 per expert.
  • Record experiments/2026-10-21-moe-batched-hot-experts.md, with data and scripts in experiments/data/moe-batched/.

Verification

  • CPU: python -m pytest -q tests — 26 passed (torch 2.14.1+cpu, transformers 5.20.2). CI green.
  • RNGD setup:
    • hardware: npu-node-1, one card per k8s Job; furiosa-torch 2026.2.0 / furiosa-llm 2026.3.0 image, torch 2.10.0+cpu, transformers 5.1.0;
    • model and recipe: Qwen3-30B-A3B-Instruct-2507, LoRA r16 q·v, S=128, hot fraction 0.25 (32 of 128 experts per layer), NSMC data;
    • method: one process, 4 host-only warm-up steps, then alternating hot / host-only steps (12 + 12), so the shared host's load drift (load average 120–150) hits both sides;
    • commits: batched 386b8bb, per-expert baseline main a07aefc.
code hot-expert step, median host-only step, median device calls/step hot / host
main (per-expert calls) 42.4 s 17.2 s 3,068 2.46×
this PR (batched) 13.0 s 13.5 s 144 0.97×
  • Micro-benchmark, one batched call with E=32, H 2048, F 768 and 64 rows per expert: upload 2.6 ms, forward 6.3 ms, backward 9.0 ms, download 7.8 ms. The host per-expert path for the same rows takes 58 ms.
  • Re-planning the hot set every step re-uploads 11–47 layers' stacked weights per step, costing 1.2–7 s. A fixed hot set uploads nothing after the first step.

Reproduce: python3 -m npu_lora.moe_bench2 MODEL DATA 0.25 out.jsonl 24, with experiments/data/moe-batched/moe_bench2.py copied into npu_lora/. Precompile the buckets first with moe_precompile.py in a CPU Job (about 40 min per bucket).

Compatibility and limits

  • Graph cache keys change (moe-experts-E… instead of moe-expert-H…), so the old per-expert graphs are not reused. Each bucket compiles in about 40 min of CPU.
  • Every hot expert is padded to the busiest one's bucket.
  • MoE hot experts: batch the hot experts of a layer into one device call #35's "faster than host-only" is not clearly met. At S=128 the batched path is at parity with host-only (median paired difference −0.4 s). The remaining step time is host work: cold experts, router and norms. Options for later:
    • more rows per call (longer sequences);
    • a larger hot fraction (E=64);
    • dropping the forward that HostMLP.backward recomputes.
  • Use a large moe_replan_every with hot experts; re-planning every step costs uploads.
  • Loss: the CPU test checks hot vs host gradients. The RNGD per-step losses were measured with main's head, before Qwen2 on RNGD: pass npu-lora check (fp32 q/k + centred k), no negative loss (host target logit) #43, which rounded the target logit, so they are not compared.

Related Issue

Related to #35 (the batched path is done; faster-than-host is still open)

🤖 Generated with Claude Code

@gspark-etri

Copy link
Copy Markdown
Contributor Author

RNGD data for this design. I implemented the same approach independently on a branch (stacked hot weights + bmm, one call per layer and direction, padded buckets) before seeing this PR, and ran it on RNGD. It is close enough to this PR that the numbers should carry over; I'm dropping my branch and adding them here.

Setup: Qwen3-30B-A3B-Instruct-2507 LoRA, hot 25% (K = 32 per layer), seq 128, RNGD chip 0, furiosa-torch 2026.2.0. The host was shared (load average 4–43 from other users' jobs).

step s/step device calls (cum.) host MoE s* note
1 205 0 202 hot set empty until the first replan
2 6914 96 – first compile: 900–1440 s per graph part (K=32; buckets 64 and 128; bwd split into 3 parts)
3 402 192 398
4 272 288 268
5 461 384 456

*host_moe_s from #32 includes the device expert calls made inside the MoE block. device_and_transfer_s (attention graphs and transfers) was about 4.5 s per step.

  • Calls. 96 device calls per step, i.e. 48 layers × 2. Your test counts 3 per layer, so the recompute in HostMLP.backward may differ between our versions.
  • Speed. Better than the per-expert path (about 615 s/step). Not shown to beat host-only on this loaded host: npu-lora check: pin MoE routing to the NPU run; step-time split host MoE vs device #32 measured 196–289 s/step host-only, and 191–281 s of that is the host MoE block. The step stays dominated by the host side: cold experts in a Python per-expert loop, bf16 matmuls on the CPU, and the forward recomputed in backward. That is likely the lever for MoE hot experts: batch the hot experts of a layer into one device call #35's "faster than host-only".
  • Compile cost. One-time but large: about 2 h for the first step with two buckets. Pre-compiling the expected buckets, or limiting the bucket set for K=32, may be worth it.

@gspark-etri
gspark-etri force-pushed the perf/35-batch-hot-experts branch 2 times, most recently from 961f4ec to 4057701 Compare October 21, 2026 10:53
gspark-etri and others added 4 commits October 21, 2026 21:56
Pack the rows routed to a layer's hot experts into [E_hot, bucket, H] and run the
experts as bmm over stacked weights [E_hot, 2F, H] / [E_hot, H, F] kept resident on
the device per layer. Cold experts stay on the host. Adds test_hot_expert_packing;
the hot-expert test now checks the call count (3 per layer, not per expert).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tats

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gspark-etri
gspark-etri force-pushed the perf/35-batch-hot-experts branch from 4057701 to f1419e4 Compare October 21, 2026 12:58
@gspark-etri
gspark-etri marked this pull request as ready for review October 21, 2026 12:58
@gspark-etri
gspark-etri merged commit b6e9ec0 into main Oct 21, 2026
1 check passed
@gspark-etri
gspark-etri deleted the perf/35-batch-hot-experts branch October 21, 2026 13:01
gspark-etri added a commit that referenced this pull request Oct 21, 2026
…ost-only on RNGD (#44)

## Background
After #38, hot experts on RNGD were at parity with host-only: 13.0 vs
13.5 s/step, alternating steps at host load 120–150. #35 is still open
for "faster than host-only". A per-step profile (Qwen3-30B-A3B, LoRA r16
q·v, S=128, hot fraction 0.25) shows where the time went:
`HostMLP.backward` recomputes the whole MoE block forward before its
autograd pass. That is about a third of the host MoE time, and one of
the three device calls per layer.

## Changes
- `npu_lora/moe.py`: `HostMLP(keep_graph=True)`. `forward` builds the
autograd graph and keeps `(x, y)` per layer. `backward` reuses it when
it receives the same input tensor, and otherwise recomputes as before.
It costs the block's saved activations until that layer's backward, a
few MB per layer at S=128.
- Tests:
- `test_host_mlp_keep_graph_equals_recompute`: LoRA input gradients and
full-FT parameter gradients are bitwise equal to the recompute path;
- the hot-expert test now expects 2 device calls per layer instead of 3.
- Record: a follow-up section in
`experiments/2026-10-21-moe-batched-hot-experts.md`, with raw data and
the profiler in `experiments/data/moe-batched/`. STATUS updated.

## Verification
CPU: `python -m pytest -q tests` (27 passed, torch 2.14.1+cpu,
transformers 5.20.2).

RNGD setup:
- hardware: npu-node-1, one card in a run-and-exit k8s Job;
furiosa-torch 2026.2.0 / furiosa-llm 2026.3.0, torch 2.10.0+cpu,
transformers 5.1.0;
- model and data: Qwen3-30B-A3B-Instruct-2507, NSMC data, seed 0, hot
set fixed after 4 host-only warm-up steps;
- method: one process; 6 rounds of 4 configurations, order reversed
every other round; load average 53;
- commit: `1fee9f2`.

| config (median step) | step | attention + head (device) | HostMLP fwd
| HostMLP bwd (of which recompute) |
|---|---:|---:|---:|---:|
| host-only (as `main`) | 9.21 s | 1.26 | 2.48 | 5.34 (2.48) |
| host-only + keep_graph | 8.00 s | 1.16 | 2.46 | 4.31 (0) |
| hot (as #38) | 6.70 s | 1.07 | 1.73 | 3.79 (1.66) |
| **hot + keep_graph** | **5.19 s** | 1.10 | 1.81 | 2.21 (0) |

Paired per round:
- hot + keep_graph minus host-only + keep_graph: **median −2.66 s
(−33%)**, 6 of 6 rounds negative;
- hot + keep_graph minus host-only as on `main`: median −4.17 s (−45%).

Reproduce: `python3 -m npu_lora.moe_prof MODEL DATA 0.25 128 6
out.jsonl`, with `experiments/data/moe-batched/moe_prof.py` copied into
`npu_lora/`.

## Compatibility and limits
- `keep_graph` is on by default. It speeds up host-only MoE too (9.21 →
8.00 s). Pass `keep_graph=False` to get the old memory profile.
- The gain depends on the shared host's load. At load 120–150 (#38's
A/B) hot and host-only were at parity without `keep_graph`; at load 53,
hot was already faster.
- Not tried: E=64 and S=256/512. Each needs about 40 min of compile per
expert bucket, and S needs new attention graphs. They were not needed
for the criterion.

## Related Issue
Closes #35

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant