prime-rl itself — running the test suite, contributing changes, and adding new model architectures with the small-scale tooling we use to iterate on MoE families without booting up a 100B+ run.
Table of Contents
Test Suite
The test suite is split into three tiers, each with its own CI workflow.Layout
tests/unit/— fast-running, hermetic tests for isolated logic: config parsing and validation, advantage / loss / scheduler / packer math, individual dataset paths, model-conversion roundtrips, etc. Tests that need a GPU are tagged with thegpumarker.tests/integration/— full-stack RL/SFT runs on a tiny model end-to-end through inference + orchestrator + trainer.tests/nightly/— runs the configs inexamples/every night to catch regressions in the shipped examples.
Running Tests Locally
CI Workflows
The GPU + Nightly workflows skip drafts — open the PR as Draft until you’re ready to consume CI compute, then mark it ready for review to trigger the GPU matrix.
Markers
Two pytest markers are declared inpyproject.toml (addopts = "--strict-markers"):
gpu— gate a test that needs CUDA. CPU CI uses-m "not gpu"; the GPU unit job uses-m gpu.slow— gate a test that’s expensive enough you’d usually skip it locally. Deselect with-m "not slow".
Pre-Commit Hooks
Install the pre-commit hooks before your first commit so ruff check + format run on staged Python files automatically:Adding a New Model
Bringing up a new model family is three steps: implement the modeling code, register a mini preset, and run the smoke test. The preset and smoke test let you iterate on the modeling code at ~0.5B scale on 1–2 GPUs instead of paying the cost of the full-size model — useful for catching bugs in modeling code, state-dict conversions, and pipeline integration before scaling.Implement the Modeling Code
Drop the modeling code undersrc/prime_rl/trainer/models/<arch>/ (HF-compatible config, modeling, and weight conversion). Mirror the layout of an existing family — glm4_moe/ or qwen3_moe/ are good starting points.
Register a Mini Preset
Add an entry toscripts/mini_moe.py so the smoke-test workflow can build a ~0.5B test model in your architecture. The preset names the config class, picks small dimensions, and wires up the HF + prime-rl model classes plus a tokenizer source:
Run the Smoke Test
Build the mini model. This creates a ~543M-parameter GLM-4 MoE (1024 hidden, 24 layers, 8 experts) with random weights, copies the tokenizer from the original GLM-4 model, and verifies the HF↔prime-rl roundtrip is lossless:- No crashes. Validates the full inference + orchestrator + trainer pipeline end-to-end.
- Finite, non-zero KL. Confirms the reference distribution is meaningful.
- Loss reasonable. Not NaN, not stuck.
Requirements for merging a new model
Before merging a new model, you need to ensure the following:- The model is correctly registered and defines and all the required methods - such as
convert_hf_layer_to_ttandconvert_tt_layer_to_hf. - The small smoke test passes.
math environment with batch_size=64. All the entries in the table must lower than 0.015. If this is not met, the PR will not be merged (unless reasonable justification is provided). This is to ensure all our models are consistent and their implementations match the implementations in the inference framework.
Adding a Custom VLM Implementation
VLM training (any run with[model.vlm] set, SFT or RL) is custom-implementation-only: get_model rejects models without a custom PrimeRL VLM class at load time. To make a new VLM family trainable, extend a custom text model with a composite VLM body — the Qwen3.5 dense (models/qwen3_5/) and MoE (models/qwen3_5_moe/) implementations are the reference. The pieces, in dependency order:
- Custom text model first. The VLM body wraps a custom
*ForCausalLM(see Adding a New Model), so the text side — including its state-dict conversion and KL-mismatch table — comes first. - Composite VLM body. A
*VLMModelthat holds the HF vision encoder and the custom text model, with aprepare_inputs_embeds_and_position_idsstep: embed tokens, run the vision encoder, scatter image embeddings over placeholder tokens, and build MRoPE 3D positions frommm_token_type_ids(the renderer owns the token→modality mapping). The unified*ForCausalLMdispatches on the config: composite config → VLM path, text config → text path. - Always run the vision encoder. Text-only micro-batches must feed the encoder dummy pixels and graft the result into the graph with zero contribution (
inputs_embeds + image_embeds.sum() * 0.0) so FSDP/EP collectives stay symmetric across ranks when the encoder is trainable. - Packed-boundary consumption. Samples pack into shared rows with per-document boundaries in
seq_lens; every custom model’sforward()declares the typedseq_lens/seq_lens_are_pre_shardparameters (the trainer passes them unconditionally) and must honor the boundaries — varlen flashcu_seqlens, linear-attention state resets per document, and a loud rejection on attention paths that can’t (see the packed-batch guard in any modeling file). Setsupports_packed_multimodal_trainingon the VLM model once packed rows are handled — RL fails loudly at startup for VLM models without it. - Registration. Register the composite
model_typein_CUSTOM_VLM_MAPPING(models/__init__.py) soget_modeldispatches to the custom class, and describe the family inVLM_REGISTRY(utils/vlm.py). - Context parallelism (optional). CP-capable VLMs implement
set_context_parallel_attributesand shard embeds/positions inside the model after the vision merge; the trainers defer sharding to the model for MRoPE batches under ulysses. - Validation. Same bar as text models: the KL-mismatch table for the text path, plus an SFT run and an RL run on a real multimodal dataset (the
color-codewordenvironment is the reference task).