This file stores repo-specific priors for future agents. Keep it short, practical, and biased toward things that save repeated exploration.
Do an improve pass for key tasks:
- new environment
- new task type
- new project area
- high risk / high cost work
- collaboration / handoff work
Improve pass:
- Identify friction
- Extract reusable priors
- Write them down in:
AGENTS.mdfor repo-wide priorsdocs/for process / runbook details
- Core package:
steptronoss/- core, model, data, exp, optimizer, generation, tokenizer, utils, checkpointing
- Experiments:
playground/ - Tests:
tests/ - Docs:
docs/
steptronoss/utils/arguments.py: config overrides from CLIsteptronoss/utils/comm_utils.py: Redis rendezvous, queues,LocalFuture/RemoteFuturesteptronoss/utils/dist_utils.py: broadcast / all-to-all helpers, packing helpers, balancing helperssteptronoss/utils/general.py: numeric helpers, list split/balance, RNG fork, retry, recursion helpers, git hashsteptronoss/utils/logger.py: rank-aware logging andStepWritersteptronoss/utils/metrics.py: metrics system (Metric,Avg,Percentage,Histogram,Text,GradNorm,GlobalMetrics)steptronoss/utils/optimizable.py:@optimizable(...)andset_optimization(...)steptronoss/utils/utils.py: model unwrap, param norms, memory report, layer map, IO helpers, generic loadsteptronoss/utils/weight_loader.py: HF safetensors mapping / merge
- Config class fields should include a short triple-quoted docstring immediately after the attribute definition.
- Follow the
configurizepattern:- class attrs declare sub-config types
- instance
__init__sets concrete values - use
Ref("..path")for cross-node linkage - configs expose
build()/build_*,sanity_check(),to_dict()
- Only
Ref(...)the exact parameter needed, not whole config objects.
- SFT experiments under
playground/sft/qwen3/*_sft_step3_data.pytypically follow:class Exp(BaseExp)model_cfg/data_cfgdeclared as class attrs- trainer / checkpoint / model fields adjusted in
__init__ - entrypoint is
if __name__ == "__main__": Exp().train()
- After
uv sync, also installredis-server:apt install -y redis-server
- Set:
CUDA_HOME=/data/cuda/cuda-12.9/cudaCUDACXX=$CUDA_HOME/bin/nvcc
- Install:
pip install -e /data/DeepEP --no-build-isolation
- Do not rely on random prebuilt wheels; ABI mismatch is common.
- Build with CUDA 12.9:
CUDA_HOME=/data/cuda/cuda-12.9/cuda CUDACXX=/data/cuda/cuda-12.9/cuda/bin/nvcc <python> -m pip install -e <grouped_gemm_source> --no-build-isolation
- Runtime constraints:
batch_sizesmust be CPU-visible /torch.int64- inputs must be bf16 for
nv_grouped_gemm
- Config class fields should include a short triple-quoted docstring immediately after the attribute definition.
- Follow the
configurizepattern:- class attrs declare sub-config types
- instance
__init__sets concrete values - use
Ref("..path")for cross-node linkage - configs expose
build()/build_*,sanity_check(),to_dict()
- Only
Ref(...)the exact parameter needed, not whole config objects.
- SFT experiments under
playground/sft/qwen3/*_sft_step3_data.pytypically follow:class Exp(BaseExp)model_cfg/data_cfgdeclared as class attrs- trainer / checkpoint / model fields adjusted in
__init__ - entrypoint is
if __name__ == "__main__": Exp().train()
playground/sft/qwen3/qwen3_sft_base.pyalready providesOneNodeResourceConfigwithreplica=1andgpu=8, so derived SFT experiments default to single-node 8-GPUtorchrununless they overrideresource_cfg.- Under
playground/data/sft, keep raw source recipes and dataset configs distinct in naming:*_recipe*.pyforDataRecipe/ source file lists only*_data_config*.pyforCompliableDatasetsConfig,CompiledDatasetsConfig, andSFTDataConfig- when adding tokenizer variants for the same source recipe, share a common base config and use thin tokenizer-specific subclasses instead of duplicating whole modules
- for large unified SFT rebuilds, do not mutate source json/jsonl in place; materialize derived json under
/oss/..., preserveDataSourceFile.subsample_ratesemantics with sample seed1234, and do global shuffle with an external bucketized sort instead of loading everything into memory
- After
uv sync, also installredis-server:apt install -y redis-server
- Set:
CUDA_HOME=/data/cuda/cuda-12.9/cudaCUDACXX=$CUDA_HOME/bin/nvcc
- Install:
pip install -e /data/DeepEP --no-build-isolation
- Do not rely on random prebuilt wheels; ABI mismatch is common.
- Build with CUDA 12.9:
CUDA_HOME=/data/cuda/cuda-12.9/cuda CUDACXX=/data/cuda/cuda-12.9/cuda/bin/nvcc <python> -m pip install -e <grouped_gemm_source> --no-build-isolation
- Runtime constraints:
batch_sizesmust be CPU-visible /torch.int64- inputs must be bf16 for
nv_grouped_gemm
steptronoss/expprovides abstract*Configinterfaces (build_*,get_trainer_cls)- Concrete configs live mainly in
steptronoss/exp/base_exp.py - Ready-made experiment families:
PretrainExp/NTPTrainerConfiginntp.pySFTExp/SFTDataConfiginsft.py- inference configs in
inference.py
- Common training configs:
AdamConfig- constant / linear / cosine schedulers
- checkpoint config (
SaveOptions,LoadOptions,CheckpointConfig)
- After creating or editing an experiment:
- run
cfshow <exp.py>to inspect the config tree - make sure
sanity_check()passes - run
mypy <exp.py>
- run
- If experiment B is derived from experiment A, use
cfshowdiff to verify changes.
- Pretrain configs live under
playground/pretrain/ playground/pretrain/step3p5/step3p5_flash.pyis the main recent Qwen3 config reference- When translating a full
ModelConfigintostep3p5_flash.py, update only existing attrs - Some keys map indirectly:
disable_qk_norm↔use_qk_norm(inverted)use_swiglu_limit↔swiglu_limit
- For
ImageForInsertmultimodal embeddings with context parallel, do image insertion on the full embedding sequence first and only then callscatter_to_balanced_cp_region(...); scatteringinput_idsbefore multimodal tok-embedding breaks insert-location alignment. steptronoss/model/common/vit.pyis now a TP-sharded vision transformer: qkv / out projection and MLP use TP-partitioned linear layers, while patch embedding and downsamplers stay replicated.- If you change
num_layers, keep all layer-wise lists in sync:qk_rope_head_dimrope_thetause_fused_qknorm_and_ropeuse_swiglu_limituse_swiglu_limit_shared
playground/eval/*currently uses a thinGenableEvalConfigwrapper, not the trainer stack.- The common eval skeleton is three roles in
resource_cfg.task_specs:router,vllm,evaluator. - Runtime flow is:
Exp.entrypoint()dispatch byROLE-> router publishesVLLM_ROUTER_ADDR_PORT_<key>in exp Redis -> vLLM worker health-checks then registers -> evaluator waits viavllm_cfg.build_cli().wait_for_server()and runseval_cfg.eval(). - Sample execution path is
GenableEvalConfig.eval()->GenerationController.generate()->SimpleTrainable.generate()-> router/v1/completions; task-specific metrics are computed in the concrete eval config, not in a shared benchmark harness. - These eval jobs depend on
STEPTRON_MEET_DIRbecauseget_exp_redis()uses the shared filesystem there for Redis rendezvous. - For a single-node eval, prefer
python tools/mp_run.py playground/eval/...py; for multi-node/manual scheduling, generate per-role scripts withpython tools/build_scripts.py playground/eval/...py <output_root>. - A fresh vLLM 0.17 eval startup can spend several minutes before
/healthopens: model inspection, distributed init, weight load,torch.compile, KV-cache sizing, and CUDA graph capture all happen before the controller can register success. Repeated controller-sideConnection refusedduring that phase is expected if logs still show forward progress. - Use
GenableItemfor generation-only eval items and reserveTrainableItemfor objects that actually implementgenerate_for_train().GenerationControllernow acceptsGenableItemon the normal generate path and only requiresTrainableItemwhenfor_train=True. - The chat eval wraps each
chat/completionsrequest withretry_on(...)for transient HTTP failures. Retry only transport errors and transient statuses (408,425,429,5xx); keep permanent4xxresponses fail-fast so bad requests are not retried blindly. steptronoss/generation/vllm/vllm_router.pynow aims to be timeout-transparent: its upstreamaiohttpsession disablestotal/connect/sock_connect/sock_readtimeouts instead of imposing an extra router-side timeout layer. Client-side timeouts still exist, but they are not propagated over HTTP; the closest transparent behavior is for the router not to add its own.GenerationController.set_tqdm(disabled, total, desc)is the supported way to customize progress output. The callback thread owns the actual tqdm instance; callers should configure it from the main thread instead of constructing ad-hoc bars around controller callbacks.- In
steptronoss/generation/async_generation.py, global generation throttling must happen in the mainGenerationControllerdispatch path, not by bounding the workermp.Queuealone. Each worker immediately drains that queue into its own local asyncio queue, so a plain queuemaxsizeis not a real global concurrency cap. Use callback/result arrival as the ack that frees one in-flight slot and dispatches the next pending genable. - In the eval, do not pass raw
max_decode_steps=max_seq_lenstraight through tochat/completions. Cap each request bymax_model_len - len(prompt.tokens); otherwise vLLM rejects every call withVLLMValidationErrorbecause the prompt leaves zero completion budget. vllm_gpu_memory_utilization=0.95can make the full mixed-benchmark eval collapse withEngineDeadError/Process EngineCore_DP* diedonce generation starts. Lowering the vLLM flag to0.85stabilized the defaultnum_generation_workers=32subset run (benchmarkdown_sample_to=1) and allowed the full run to start cleanly without immediate OOM spam.- If you introduce benchmark abstractions on the OSS side, keep the base protocol under
steptronoss/generation/base_benchmark.py, and put concrete benchmark implementations underplayground/eval/benchmarks/<BenchmarkName>/. Avoid hiding benchmark selection behind a registry when the benchmark set is still evolving quickly; explicit construction in the eval exp is easier to audit and refactor. - Shared simple-benchmark eval plumbing now lives in
playground/eval/eval_sets/simple_eval.py. That module owns the simple-benchmark list itself; model-specific eval files should only bind model/resource/tokenizer config on top ofSimpleBenchmarksEvalConfig. - Some Step3/Step3.5 training exports under
/oss/checkpoints/.../hfcontain only safetensor shards plusmodel.safetensors.index.json, withoutconfig.jsonor tokenizer assets. Those raw dirs are not directly serveable by vLLM; prepare a wrapper HF dir (for examplehf_vllm) that adds a compatibleconfig.json, and pointtokenizer_pathat a separate mounted tokenizer. - Sampling policy for the shared simple-benchmark eval should live in
SimpleBenchmarksEvalConfig.get_sampling_params(...), not be hardcoded insideSimpleChatGeneratable. Keep the generatable responsible only for per-request normalization such as context-budget clamping and filling a default seed when the config leaves it unset. - Benchmark-focused tests under
tests/should live intests/benchmarks/instead of the top-leveltests/directory, so benchmark wrappers and their fixtures stay grouped together. - Benchmark-specific code should stay inside its own benchmark folder under
playground/eval/benchmarks/<BenchmarkName>/; avoid spreading benchmark logic, helper modules, or downloaded benchmark assets into unrelated directories. - Benchmark class initialization must stay lightweight. Prefer lazy import, lazy data parsing, and lazy verifier/resource setup; importing a benchmark module or constructing the benchmark object should not trigger heavyweight package imports, network access, or resource downloads.
- Benchmark resources should live under one explicit root path agreed for that benchmark, and the benchmark class should receive that path through initialization parameters or derive it from a caller-provided parent such as
datasets_dir. Do not hide resource paths across multiple hardcoded locations. - If a benchmark depends on external resources beyond Python packages, such as NLTK corpora/models, download them ahead of time into that benchmark resource root and have runtime code read from there. Do not rely on import-time auto-download behavior.
playground/eval/benchmarks/IFBench/benchmark.pyshould lazy-import the official AllenAI verifier fromplayground/eval/benchmarks/IFBench/official/and derive its explicit resource root from the caller-provided simple-benchmarkdatasets_dir, using<datasets_dir>/IFBENCH/forIFBench_test.jsonlplusnltk_data/.simple_evalshould pass that directory root directly, and the benchmark should only accept that directory-root form instead of carrying compatibility for explicit prompt-file paths. Do not keep a second hardcoded IFBench resource root in the benchmark or helper modules. Keep NLTK/resource setup lazy too; do not trigger imports, downloads, or directory creation at module import time. It defaults to officialloosescoring and strips inline<think>...</think>-style reasoning before verification; rollout/sampling settings still come fromsimple_eval, so leaderboard parity still requires matching the official generation settings such astemperature=0.- Keep IFBench benchmark-owned sampling overrides narrow.
playground/eval/benchmarks/IFBench/benchmark.pyshould pin official settings liketemperature=0, but should not hardcodeextra_body.chat_template_kwargs; IFBench thinking/chat-template behavior should flow fromSimpleBenchmarksEvalConfig.chat_template_args. - For vendored official benchmark helpers such as
playground/eval/benchmarks/IFBench/official/, keep only the runtime scoring path needed by the OSS benchmark wrapper. Script-style file I/O helpers, report printers, and other standalone-binary scaffolding from the upstream repo are dead weight unless the OSS call path actually invokes them.
- Global
PMinsteptronoss.core.parallel_stateis theParallelManager - Typical flow:
PM.initialize()PM.set_mesh(parallel_cfg)- or
with PM.use_mesh(parallel_cfg): ...
- Common helpers:
PM.define_parallel(pattern, **sizes)PM.size_of("TP")PM.rank_in("DP")PM.group_of("PP")PM.ranks_of("EP")
- VPP uses:
virtual_pipeline_model_parallel_sizeget_vpp_rank()set_vpp_rank()
model_cfg.pipeline_activation_cpu_offloadapplies to bothPPSchedulerandVPPScheduler; it is implemented withtorch.autograd.graph.save_on_cpu(...), currently requiresmodel_cfg.recompute=True, and should be treated as graph-preserving activation offload rather than a replacement forrecompute.
ParallelConfig.sanity_check()requires:WORLD_SIZEdivisible by attention MP size =PP * TP * CPWORLD_SIZEdivisible by MoE MP size =PP * ETP * EP
- For an 8-GPU run with
TP=8andEP=8, set:expert_tensor_parallel_size=1- otherwise MoE MP size becomes 64 and the config is invalid
- In mixed dense/MoE topologies, expert params are reduced over
EDP, not denseDP; the current gradient manager compensates withTP/EPscaling on expert grad buffers before theEDPreduction, so check that path before blaming an apparent extraEPfactor. MeshConnectortreats ranks that differ only in its configurabledup_dimas duplicate payload holders; Step3V passesdup_dim=["TP"]. Callers should keep all ranks on the sameforward()/backward()sequence; empty batch shards skip the auxiliary encoder but still participate in connector return traffic.
steptronoss/checkpointing/reshape_ops.pycontains reshape primitives:VocabPadColumnParallel/RowParallelKeepThisTP/KeepThisEPGQAMergeQKVFFNMergeGateUpUnbindMoERenameInverse
- Typical usage:
- build
Script(src=..., op=..., dst=...) - return
OnlineReshaper(scripts)
- build
- For expert slicing from per-expert keys:
- use
Inverse(UnbindMoE(...)) + KeepThisEP()before TP ops
- use
TrainableItemshould not pickle / serialize tokenizer instances; drop them in__getstate__model_nameoften includesexp_id; persist a template likedeployed-model-{EXP_ID}so resume survivesexp_idchanges- A temporary synthetic-latency harness was useful for proving that fully-async can outperform one-step-off in a balanced-base regime, but that experiment-only sleep code has been removed from the shared RLVR files. Keep future synthetic-latency instrumentation outside the main experiment code unless it is intended to stay.
- In
steptronoss/core/generators/flow_controller_simulator.py,max_concurrentnow means real concurrent prompt progress for every strategy: simple strategies use it as a per-block infer slot cap, andfully-asyncadvances every running prompt each tick up to that cap. steptronoss/core/generators/flow_controller_simulator.pynow appliesmax_concurrentconsistently across strategies. For simple strategies, an explicitmax_concurrentcaps per-block infer concurrency; forfully-async, every running prompt advances each tick up to that concurrency cap (not the old single-prompt RR service bug).- For short RLVR timing A/Bs (
train_itersaround 7),fully-asynccan look worse even when its simulator steady state is better, because the final drain/tail batch dominates the average. For flow-policy comparisons, prefer more iterations or compare middle steady-state steps separately from warmup/drain. steptronoss/core/generators/flow_controller.pyruns rollout blocks sequentially on the inference side but can overlap the next block with trainer compute. Within a block, prompts are submitted concurrently and block duration is effectively bounded by the slowest prompt.SimpleFlowControllerversion scheduling is0, 1, 2, ...foron-policyand0, 0, 1, 2, ...forone-step-off; after warm-up,one-step-offtypically trains iterkon rollouts generated by actor versionk-1.FlowControllerConfigis now the simple-controller base foron-policy/one-step-off, whileFullyAsyncFlowControllerConfigcarriesfully-async-specific knobs (max_untrained_prompts,max_staleness) and dispatches toFullyAsyncFlowController. The current fully-async implementation is a first pass: it schedules prompts with backpressure, gates version bumps by staleness, and yields oncepre_trainhasprompt_per_iterprompt results, but it should still be validated against the simulator before behavior-changing edits.steptronoss/core/generators/flow_controller_simulator.pyis now the safest place to reason about new fully-async rollout rules first. Itsfully-asyncpath modelsprompt_per_iter,max_untrained_prompts,max_staleness, and an explicitmax_concurrentconcurrent-prompt scheduler before the real controller exists.
- Use
MuonConfig.mark_muon_params(model)before grouping - In experiments, prefer overriding
optimizer_cfgvia aGradientManagerConfigsubclass that setsoptimizer_cfg = MuonConfig - Leave distributed optimizer on, but avoid byte-level sharding
- For Muon tests, prefer composing existing reshape ops instead of inventing new ones
- In
playground/sft/step3/*muon*,Step3p5MuonConfig.mark_muon_paramsinlines the base Muon selection rules but tags trainable params withndim >= 2as Muon candidates (still respecting embedding/name exclusions); Step3.5 FlashGroupedExpertsmerge ops useUnbindMoE + Inverse(Column/RowParallel + KeepThisTP(group="ETP")).
- Follow:
docs/TRITON_ACCELERATION_WORKFLOW.mddocs/TRITON_ACCELERATION_WORKFLOW_ZH.md
- Rules:
- optimized implementations belong under
steptronoss/model/optimizations/* - semantic entrypoints stay in
steptronoss/model/utils/* - expose alternatives through
@optimizable(...) - alternatives must be strict drop-in replacements
- add correctness tests, backward-aware benchmarks, and a short real experiment trace
- optimized implementations belong under
rgmay be unavailable; fall back tofind/greppythonmay be missing andpython3may not includepytest; prefer project tooling if available- Some tensor-parallel model builders allocate on CUDA unconditionally; for CPU-only smoke tests around Step3.* multimodal
forward_head/ reshaper behavior, prefer a thin toy wrapper that reuses the real model methods with CPU-safe fake embeddings instead of building the full model. tests/conftest.pynow applies a shared skip to every@pytest.mark.node2test unless the run is launched undertorchrun --nproc-per-node=2; plainpytestshould skip them instead of hanging in distributed init.
- This environment may not have worker / GPU access; avoid running GPU-only tests when the machine does not actually have GPUs
@pytest.mark.node2tests should also usepytest.mark.xdist_group("torchrun")- Test layout:
- single-node GPU tests:
tests/test_muon_optimizer.py - 2-node GPU tests:
tests/test_muon_optimizer_node2.py
- single-node GPU tests:
steptronoss/model/ep_dispatcher/deepep_dispatcher.pymust keeprecv_token_probsdifferentiable and passgrad_recv_token_probsintobuffer.combine(...); otherwise router main-loss gradients are cut whenTokenDispatcher="deep_ep".
steptronoss.utils.memory_tracker.CMTonly records whenMEM_DIAGNOSE=1- If training hangs on
Waiting for debugger... ip: ... rank: 56, check for a straydebug(56)insteptronoss/core/trainers/lm_trainer.py - TorchDynamo graph breaks are often triggered by
Tensor.item()in optimizable helpers; prefer tensor-safe checks like maskedamax+torch._assert steptronoss/model/common/rope.pyshould keep RoPE cos/sin caches and cache-generation math intorch.float32; module-wide.to()/cuda()/bfloat16()may move the cache device, but must not downcast the cache dtype.