8000 Grouped expert dispatch: no padding to the busiest expert by gt50 · Pull Request #4 · gt50/mini-AGI · GitHub
[go: up one dir, main page]

Skip to content

Grouped expert dispatch: no padding to the busiest expert - #4

Merged
gt50 merged 1 commit into
mainfrom
grouped-dispatch
Oct 4, 2026
Merged

gt50 merged 1 commit into
mainfrom
grouped-dispatch

Conversation

@gt50
@gt50 gt50 commented Oct 4, 2026
Copy link
Copy Markdown
Owner

Summary

Fixes the large Strix profile slowing to half speed during training and then crashing.

Cause. Routing grows "sink" experts that nearly every character picks among its top 8. On the trained checkpoint, the busiest expert received 1,960 of 2,081 characters at a typical row, 15× the fair share with 128 experts on the card. The dispatch padded every expert to the busiest one's load:

  • Uncapped: it computed about 16× the routed work, and its buffers kept growing until an allocation failed.
  • Capped: it dropped 30–60% of routing.

Fragmentation made it worse. ROCm on Windows ignores expandable_segments, and a new buffer size on every call fragmented 17 GB of live tensors into 49 GB reserved.

Changes

  • minagi/pool.py, grouped dispatch: the chosen experts are grouped by load and each group runs its own batched matmuls at its own size. Nothing is dropped that capacity_factor didn't drop, and the work is at most about 2× the routing's instead of n × the worst expert's.
    • Weights are gathered once per call and split into the groups' views. My first version gathered per group and was slower, because each gather's backward allocates a gradient the size of the whole card.
    • One character is a single group of top-k experts.
    • The old rectangle stays available behind PooledMLP.grouped_dispatch = False.
  • minagi/pool.py, _bucket: buffer capacities are rounded to a few dozen sizes (powers of two up to 128, then multiples of 128) so the allocator reuses blocks.
  • train.py: allocated, reserved and peak GPU memory in the history file at every checkpoint.
  • tools/gpu_mem_sampler.ps1: per-process dedicated and shared GPU memory from Windows' performance counters, sampled every 30 s.
  • Profiles and README: expandable_segments is ignored on Windows ROCm, and why capacity_factor stays 0 on the large profile.

Results (Radeon 8060S, Windows, ROCm 7.13 nightly, large model at step 1,938, uncapped)

before after
training step 475 char/s, 24.6 GB peak 813 char/s, 18.8 GB peak
writing 5.3 char/s 19.8 char/s
routing dropped 0% 0%

45-minute run on this code:

  • steps per 5 minutes: 147, 115, 120, 106, 113, 101, 104, 101 (58 before the fix)
  • PyTorch reserved memory flat at 22.8 GB from the first checkpoint on
  • Windows dedicated memory flat at 22.8 GB, shared 0.1 GB
  • held-out loss 2.17 → 2.00
  • clean exit and save

The step rate eases while memory stays fixed, which fits the model learning to use more recurrent rows.

Not addressed here

With nearly the whole pool on the card, the balance term charges experts by admission share, which is close to uniform, so it can't see sink experts. Making it see routing load changes training behavior and needs its own test.

Test plan

  • pytest: 22/22. tests/test_dispatch.py checks that the grouped path matches the rectangle's outputs and slot gradients, for one character and for an uneven-load chunk that splits into ≥ 2 groups, and that bucket padding changes nothing
  • GPU A/B on the trained checkpoint (above)
  • 45-minute memory-logged training run (above)
  • CI green

🤖 Generated with Claude Code

Training the large Strix profile slowed to half speed as it went and then
crashed. Measured on its checkpoint: routing grows "sink" experts that
nearly every character picks among its top 8 - 1,960 of 2,081 characters
at a typical row, 15x the fair share with 128 experts on the card. The
dispatch padded every expert to the busiest one's load, so uncapped it
computed ~16x the routed work and its buffers kept growing; capped, it
dropped 30-60% of routing.

- minagi/pool.py: the chosen experts are grouped by load (rounded with
  _bucket) and each group runs its own batched matmuls at its own size.
  Weights are gathered once per call and split into the groups' views; a
  gather per group was slower, because each one's backward allocates a
  gradient the size of the whole card. One character is a single group of
  top_k experts, so writing reads top_k experts' weights. The old single
  rectangle stays behind PooledMLP.grouped_dispatch = False.
- Buffer capacities are rounded to a few dozen sizes (_bucket). ROCm on
  Windows ignores expandable_segments, and a new buffer size every call
  fragmented 17 GB of live tensors into 49 GB reserved.
- train.py: allocated / reserved / peak GPU memory in the history file at
  every checkpoint.
- tools/gpu_mem_sampler.ps1: per-process dedicated and shared GPU memory
  from Windows' performance counters.
- Profiles and README: expandable_segments is ignored on Windows ROCm, and
  why capacity_factor stays 0 on the large profile.

On the 8060S, step 1,938 of the large model, uncapped:
  training step  475 -> 813 char/s, peak 24.6 -> 18.8 GB, 0% dropped
  writing        5.3 -> 19.8 char/s
A 45-minute run held 100-147 steps per 5 minutes (58 before) with
reserved memory flat at 22.8 GB; held-out 2.17 -> 2.00.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gt50
gt50 merged commit 1f32ed4 into main Oct 4, 2026
2 checks passed
@gt50
gt50 deleted the grouped-dispatch branch October 4, 2026 22:20
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

0