MiniMax
Training MiniMax-M2 on H200
Ranked by tokens/s per GPU · 8,192-token SFT rows · global batch 128 · BF16
Notes for MiniMax-M2
- Weights: the FP8 checkpoint
MiniMaxAI/MiniMax-M2(revision 757303d4), cast to BF16 with slime'stools/fp8_cast_bf16.pyand converted to Megatrontorch_distwith slime'stools/convert_hf_to_torch_dist.pyand the recipes'MODEL_ARGSon one 8-GPU node. The BF16 directory also holds the checkout's tokenizer files,chat_template.jinja,generation_config.json, the remote-code filesconfiguration_minimax_m2.pyandmodeling_minimax_m2.py, and itsconfig.jsonwithoutquantization_config.--hf-checkpointis the BF16 directory and--ref-loadthe conversion. - The conversion needs one edit to slime 0.3.0 on Megatron-LM commit 1dcf0daf: in
slime_plugins/mbridge/minimax_m2.py,MiniMaxM2Bridge._build_configpassesrotary_percent=0.5to_build_base_config, which hands it to Megatron'sTransformerConfig. That class has norotary_percentfield, so the conversion stops with aTypeError. Delete that keyword argument before converting. The bridge's config only maps the weights; training builds the model from--rotary-percent 0.5inMODEL_ARGS. MODEL_ARGScome from slime'sscripts/models/minimax-m2.shat slime 0.3.0. They select slime's MiniMax-M2 layer spec (--spec slime_plugins.models.minimax_m2 get_minimax_m2_layer_spec) and the alltoall dispatcher.- It trains on its own rows, built with
--loss-mask-type minimax_m2(data/README.md). - Load the checkpoint's config and tokenizer once on each node before the training processes start (
AutoConfig.from_pretrainedandAutoTokenizer.from_pretrainedon the BF16 directory withtrust_remote_code=True). Otherwise the processes of a node import the remote-code moduleconfiguration_minimax_m2at the same time, and one can stop with anAttributeErrorforMiniMaxM2Config.
The steps every recipe shares: Running a recipe.
Recipes
#1attune-tp1-pp8-ep8-vpp42,034.4 tokens/s/GPUTP1 PP8 VPP4 EP8 DP8VPP 4, no offload
- Starting point: slime's MiniMax-M2 script,
scripts/run-minimax-m2.shwithscripts/models/minimax-m2.shat slime 0.3.0. slime ships it as a 16-node GRPO script that shares the GPUs with its rollout engine and publishes no tokens/s figure for it. Ported to this workload on 64 H200 with its layout (TP 2 with sequence parallel, PP 2, full recompute, Adam on the CPU) and EP 8 in place of its EP 4, at which each GPU's expert weights and FP32 gradients alone exceed its memory, it measured 557.8 tokens/s/GPU; ATTUNE tuned it to 2,034.4 (+265%). --pipeline-model-parallel-layout 'Et|(tt|)*30tL'cuts the 62 layers into 32 virtual stages, 4 per pipeline rank: the first holds the embedding and 1 layer, the last 1 layer and the loss, and the 30 in between 2 layers each, so ranks 0 and 7 hold 7 layers and the others 8.- The reference recipe runs at TP 2 with sequence parallelism, where slime's MiniMax-M2 layer spec gives the full-width q and k norm weights twice their gradient: each TP rank applies the norm to the gathered q and k, and Megatron sums the gradients of these sequence-parallel weights over TP. The forward pass is unchanged, and this recipe, at TP 1, is not affected.
- Author
- ATTUNE (cite)
- GPUs
- 64 x H200 141 GB (AWS p5en.48xlarge, 8 per node)
- Layout
- TP 1, PP 8 (first and last stage 7 layers, the others 8), VPP 4, CP 1, EP 8, ETP 1, DP 8; one 8,192-token row per microbatch; full recompute (uniform, 1 layer); precision-aware distributed optimizer; no offload; alltoall dispatcher
- Tokens/s per GPU
- 2,034.42
- $ / 1M tokens
- $1.08
- MFU
- 12.8% (N = 10,415,112,192 active parameters per token)
- Step-time CV
- 1.57%
- Convergence check
- correlation 0.9997869, final loss difference 0.0018, 0 pointwise failures over 20 steps
- Component test
- precision-aware Adam on the GPU and CPU optimizer offload (fraction 1.0, overlapped copies), each against an FP64 reference: H200, 2026-10-11. Shape: one Qwen3.5-397B-A17B MoE layer: hidden 4096, 512 experts, top-10, expert FFN 1024, EP 8, ETP 1, 4,096 tokens per rank. worst tensor (update) relative L2 error 5.12e-7 on the GPU and 7.98e-7 offloaded against a bound of 1.0e-3 after one step, and 4.99e-5 and 7.72e-5 against 1.1e-3 after three steps at the recipe's hyperparameters (bound 2 x the default GPU optimizer's error + 1e-3); reconstructed FP32 masters bitwise identical to an arm with explicit FP32 masters, the TE remainder self-test passes and the negative controls fail run on this recipe's training image (the same image configuration); the optimizer step is elementwise, so the result holds for every model on the image
- Measured
- 2026-10-21
- GPU-hours
- 9.07
- Rows
minimax-m2-tulu3-fixed8192-v1(rows_sha256f731270d13a2b34202cc0266c58a599318ff5957b15c3f9b9e9c1f71f9caf400)
Stack
- framework
- slime 0.3.0: THUDM/slime a897e1f plus 8 commits that do not change the training step
- megatron lm
- commit 1dcf0daf (megatron-core 0.16.0rc0), with a backported upstream fix for the Qwen3.5 attention output gate at TP above the query-group count
- transformer engine
- 2.10.0
- pytorch
- 2.11.0 (CUDA 12.9 build)
- cuda
- 12.9
- nccl
- 2.28.9 (CUDA 12.9 build; printed by the measured run)
- deepep
- 1.2.1 (installed; the alltoall dispatcher does not use it)
- python
- 3.12.3
- network
- 8 GPUs per node, EFA, 3.2 Tb/s RDMA per node
Environment
CUDA_DEVICE_MAX_CONNECTIONS=1PYTORCH_CUDA_ALLOC_CONF=expandable_segments:TrueTORCHINDUCTOR_COMPILE_THREADS=1NCCL_NVLS_ENABLE=$([ "$NVLINK_COUNT" -gt 0 ] && echo 1 || echo 0)
export CUDA_DEVICE_MAX_CONNECTIONS=1
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
export TORCHINDUCTOR_COMPILE_THREADS=1
export NCCL_NVLS_ENABLE=$([ "$NVLINK_COUNT" -gt 0 ] && echo 1 || echo 0)
python3 -m <slime entry module> --async \
--actor-num-nodes 8 \
--actor-num-gpus-per-node 8 \
${MODEL_ARGS[@]} \
--hf-checkpoint "$HF_CKPT" \
--ref-load "$TORCH_DIST" \
--rollout-function-path pretokenized_sft_rollout.generate_rollout \
--prompt-data "$SFT_DATA" \
--input-key messages \
--metadata-key metadata \
--num-rollout 20 \
--rollout-batch-size 128 \
--global-batch-size 128 \
--loss-type sft_loss \
--calculate-per-token-loss \
--disable-compute-advantages-and-returns \
--debug-train-only \
--seq-length 8192 \
--tensor-model-parallel-size 1 \
--pipeline-model-parallel-size 8 \
--context-parallel-size 1 \
--expert-model-parallel-size 8 \
--expert-tensor-parallel-size 1 \
--recompute-granularity full \
--recompute-method uniform \
--recompute-num-layers 1 \
--use-dynamic-batch-size \
--max-tokens-per-gpu 8192 \
--optimizer adam --lr 1e-5 --lr-decay-style cosine --min-lr 1e-6 \
--lr-warmup-fraction 0.1 --weight-decay 0.1 --adam-beta1 0.9 --adam-beta2 0.98 \
--use-distributed-optimizer \
--use-precision-aware-optimizer \
--attention-dropout 0.0 --hidden-dropout 0.0 \
--accumulate-allreduce-grads-in-fp32 --attention-softmax-in-fp32 \
--attention-backend flash \
--distributed-timeout-minutes 30 \
--tensorboard-dir "$PROFILE_DIR" \
--pipeline-model-parallel-layout 'Et|(tt|)*30tL'
# recipes/minimax-m2/h200/attune-tp1-pp8-ep8-vpp4.yaml
# An OpenRecipes recipe; the fields are documented in schema/recipe.schema.yaml.
schema_version: 1
model: MiniMax-M2
gpu:
type: H200
memory_gb: 141
count: 64
per_node: 8
instance: AWS p5en.48xlarge
author:
name: ATTUNE
url: https://arxiv.org/abs/2610.17528
status: verified
stack:
framework: 'slime 0.3.0: THUDM/slime a897e1f plus 8 commits that do not change the training step'
megatron_lm: commit 1dcf0daf (megatron-core 0.16.0rc0), with a backported upstream fix for the Qwen3.5 attention output
gate at TP above the query-group count
transformer_engine: 2.10.0
pytorch: 2.11.0 (CUDA 12.9 build)
cuda: '12.9'
nccl: 2.28.9 (CUDA 12.9 build; printed by the measured run)
deepep: 1.2.1 (installed; the alltoall dispatcher does not use it)
python: 3.12.3
network: 8 GPUs per node, EFA, 3.2 Tb/s RDMA per node
source: 'in-pod version check of the measured run''s training image (same digest), 2026-10-22: installed TE, PyTorch,
DeepEP and megatron-core, Megatron-LM git commit, Python, and slime''s MiniMax-M2 model and launcher scripts; NCCL
from the measured run''s log'
workload:
row_tokens: 8192
rows: SFT conversations
global_batch: 128
precision: bf16
dataset:
name: Tulu 3 SFT mixture
source: allenai/tulu-3-sft-mixture
revision: b14afda60f1bbebe55d5d2fa1e4df5042f97f8be
license: ODC-By 1.0
rows_sha256: f731270d13a2b34202cc0266c58a599318ff5957b15c3f9b9e9c1f71f9caf400
build:
name: minimax-m2-tulu3-fixed8192-v1
tokenizer: MiniMax-M2 (revision 757303d492a50514c312788b5247a4f696a4c6a3; tokenizer.json sha256 757622126525aeeb131756849d93298070ff3f0319c455ec8c5bb0f6b1cebbe8;
chat_template.jinja sha256 86cd6e574dec52bf6d7e678c5b46ceb5223d68b3a1130bbbc09d0a2dc0909438)
loss_mask: minimax_m2
rows: 6400
row_tokens: 8192
padding_tokens: 0
seed: 42
packing: source conversations in a seeded global permutation, concatenated with an EOS token between them and cut
into consecutive 8,192-token rows; rows without supervised targets are skipped
supervised_token_ratio: 0.64
parquet_sha256: 2fb29ca63b79dc153defaacba1b41c783840de528a3ec353e5f6505144575036
parquet_bytes: 75539521
builder: data/generate_fixed_sft.py with --loss-mask-type minimax_m2 (data/README.md)
layout:
label: TP1 PP8 VPP4 EP8 DP8
summary: TP 1, PP 8 (first and last stage 7 layers, the others 8), VPP 4, CP 1, EP 8, ETP 1, DP 8; one 8,192-token row
per microbatch; full recompute (uniform, 1 layer); precision-aware distributed optimizer; no offload; alltoall dispatcher
highlights: VPP 4, no offload
tp: 1
pp: 8
vpp: 4
cp: 1
ep: 8
etp: 1
dp: 8
sequence_parallel: false
layer_split:
first: 7
last: 7
max_tokens_per_gpu: 8192
rows_per_microbatch: 1
recompute:
granularity: full
method: uniform
num_layers: 1
offload: null
dispatcher: alltoall
result:
tokens_per_s_per_gpu: 2034.42
usd_per_million_tokens: 1.08
step_time_cv_percent: 1.57
mfu: 0.1285
mfu_note: null
active_params:
count: 10415112192
source: https://huggingface.co/MiniMaxAI/MiniMax-M2
revision: 757303d492a50514c312788b5247a4f696a4c6a3
measured: 2026-10-21
steps_per_run: 20
window:
- 5
- 19
window_tokens: 15728640
runs: 1
profiler: false
gpu_hours: 9.07
gpu_hours_note: GPUs x (scheduler admission to finish), including startup; the same run is the candidate arm of the
convergence check
correctness:
result: pass
summary: passed; 1 component test passed on this image
reference: 'The layout of slime''s `scripts/run-minimax-m2.sh` on 64 GPUs: TP 2 with sequence parallelism, PP 2, EP
8 in place of its EP 4, ETP 1, full recompute (uniform, 1 layer), max 8,192 tokens/GPU (one row per microbatch) and
CPU optimizer offload with overlapped copies and the precision-aware optimizer. Model arguments, rows and schedule
are the same in both recipes.'
configuration: pass
component_tests:
- test: precision-aware Adam on the GPU and CPU optimizer offload (fraction 1.0, overlapped copies), each against an
FP64 reference
shape: 'one Qwen3.5-397B-A17B MoE layer: hidden 4096, 512 experts, top-10, expert FFN 1024, EP 8, ETP 1, 4,096 tokens
per rank'
gpu: H200
date: 2026-10-11
result: pass
detail: worst tensor (update) relative L2 error 5.12e-7 on the GPU and 7.98e-7 offloaded against a bound of 1.0e-3
after one step, and 4.99e-5 and 7.72e-5 against 1.1e-3 after three steps at the recipe's hyperparameters (bound
2 x the default GPU optimizer's error + 1e-3); reconstructed FP32 masters bitwise identical to an arm with explicit
FP32 masters, the TE remainder self-test passes and the negative controls fail
note: run on this recipe's training image (the same image configuration); the optimizer step is elementwise, so the
result holds for every model on the image
convergence:
steps: 20
result: pass
correlation: 0.9997869
final_loss_difference: 0.0018
pointwise_failures: 0
note: 'candidate arm: the measured run; reference arm: a 20-step run of the reference recipe on the same image and
rows; the parity probe off in both'
rules: argument classes of ATTUNE's skill/scripts/correctness/rules/numerics_list.yaml (amazon-science/attune aff5b81);
20-step check by Megatron-Bridge validate_convergence with its pipeline thresholds
env:
CUDA_DEVICE_MAX_CONNECTIONS: '1'
PYTORCH_CUDA_ALLOC_CONF: expandable_segments:True
TORCHINDUCTOR_COMPILE_THREADS: '1'
NCCL_NVLS_ENABLE: $([ "$NVLINK_COUNT" -gt 0 ] && echo 1 || echo 0)
patches: []
run: |
export CUDA_DEVICE_MAX_CONNECTIONS=1
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
export TORCHINDUCTOR_COMPILE_THREADS=1
export NCCL_NVLS_ENABLE=$([ "$NVLINK_COUNT" -gt 0 ] && echo 1 || echo 0)
python3 -m <slime entry module> --async \
--actor-num-nodes 8 \
--actor-num-gpus-per-node 8 \
${MODEL_ARGS[@]} \
--hf-checkpoint "$HF_CKPT" \
--ref-load "$TORCH_DIST" \
--rollout-function-path pretokenized_sft_rollout.generate_rollout \
--prompt-data "$SFT_DATA" \
--input-key messages \
--metadata-key metadata \
--num-rollout 20 \
--rollout-batch-size 128 \
--global-batch-size 128 \
--loss-type sft_loss \
--calculate-per-token-loss \
--disable-compute-advantages-and-returns \
--debug-train-only \
--seq-length 8192 \
--tensor-model-parallel-size 1 \
--pipeline-model-parallel-size 8 \
--context-parallel-size 1 \
--expert-model-parallel-size 8 \
--expert-tensor-parallel-size 1 \
--recompute-granularity full \
--recompute-method uniform \
--recompute-num-layers 1 \
--use-dynamic-batch-size \
--max-tokens-per-gpu 8192 \
--optimizer adam --lr 1e-5 --lr-decay-style cosine --min-lr 1e-6 \
--lr-warmup-fraction 0.1 --weight-decay 0.1 --adam-beta1 0.9 --adam-beta2 0.98 \
--use-distributed-optimizer \
--use-precision-aware-optimizer \
--attention-dropout 0.0 --hidden-dropout 0.0 \
--accumulate-allreduce-grads-in-fp32 --attention-softmax-in-fp32 \
--attention-backend flash \
--distributed-timeout-minutes 30 \
--tensorboard-dir "$PROFILE_DIR" \
--pipeline-model-parallel-layout 'Et|(tt|)*30tL'
notes: |
- Starting point: slime's MiniMax-M2 script, `scripts/run-minimax-m2.sh` with `scripts/models/minimax-m2.sh` at
slime 0.3.0. slime ships it as a 16-node GRPO script that shares the GPUs with its rollout engine and publishes no
tokens/s figure for it. Ported to this workload on 64 H200 with its layout (TP 2 with sequence parallel, PP 2,
full recompute, Adam on the CPU) and EP 8 in place of its EP 4, at which each GPU's expert weights and FP32
gradients alone exceed its memory, it measured 557.8 tokens/s/GPU; ATTUNE tuned it to 2,034.4 (+265%).
- `--pipeline-model-parallel-layout 'Et|(tt|)*30tL'` cuts the 62 layers into 32 virtual stages, 4 per pipeline rank:
the first holds the embedding and 1 layer, the last 1 layer and the loss, and the 30 in between 2 layers each, so
ranks 0 and 7 hold 7 layers and the others 8.
- The reference recipe runs at TP 2 with sequence parallelism, where slime's MiniMax-M2 layer spec gives the
full-width q and k norm weights twice their gradient: each TP rank applies the norm to the gathered q and k, and
Megatron sums the gradients of these sequence-parallel weights over TP. The forward pass is unchanged, and this
recipe, at TP 1, is not affected.
Citation
If you use a recipe by ATTUNE, please cite the paper.
@article{fan2026attune,
title = {{ATTUNE: Agentic Throughput Tuning for LLM Training}},
author = {Leo Fan and Hongyang Wang and Wenbo Chen and Jingguo Liu and Xian Li and Xuan Wang and
Ruochen Wen and Yiming Yan and Hao Wu and Zheshen Wang and Benoit Dumoulin},
journal = {arXiv preprint arXiv:2610.17528},
year = {2026},
url = {https://arxiv.org/abs/2610.17528}
}
MiniMax-M2 on other GPUs
H100
–
No recipe yet
H200 · 64 GPUs · this page
2,034.4
tokens/s/GPU · $1.08 / 1M tokens
B200
–
No recipe yet