Architecture overview
July 15, 2026 · View on GitHub
This document describes how the Primus training framework is structured: CLI and configuration, the core runtime orchestrator, backend adapters, trainer lifecycle, and the patch system.
1. System overview
Primus is organized into three conceptual layers: runtime launch (how processes and GPUs are started), hooks and patches (environment and in-process adjustments), and task execution (CLI subcommands that drive training and utilities).
┌─────────────────────────────────────────────────────┐
│ Runtime Layer (runner/) │
│ direct | container | slurm │
│ GPU detection, env setup, distributed launch │
├─────────────────────────────────────────────────────┤
│ Hook / Patch System │
│ runner/helpers/hooks/ | primus/core/patches/ │
│ Pre/post processing, runtime monkey-patches │
├─────────────────────────────────────────────────────┤
│ Task Execution Layer │
│ primus/cli/subcommands/ │
│ train | benchmark | preflight | projection │
└─────────────────────────────────────────────────────┘
The repository also provides shell entrypoints under runner/ (for example primus-cli-direct.sh, primus-cli-container.sh, primus-cli-slurm.sh) that prepare the environment and invoke the Python CLI.
2. CLI and plugin system
- Entry point:
primus/cli/main.pyis the unified CLI entry. It discovers subcommand modules underprimus/cli/subcommands/withpkgutil.walk_packages, skipping modules whose leaf name starts with_. - Registration contract: Each subcommand module exposes
register_subcommand(subparsers)and must return the configured parser. The parser must callset_defaults(func=run)somain()can dispatch to the handler. - Parsing: The CLI uses the standard library
argparseonly (no Click or Typer). - Unknown arguments:
main()callsparse_known_args(). For selected subcommands (train,projection,preflight), trailing tokens are passed through to the handler as overrides; for other commands, unknown arguments are rejected.
3. Configuration pipeline
Configuration flows from experiment YAML to a resolved structure consumed by the runtime.
- CLI parses
--config/--exp(required for train flows) pointing at an experiment YAML file. load_primus_config()(used byPrimusRuntime) delegates toPrimusParser.parse()inprimus/core/launcher/parser.py. The parser loads the experiment file viayaml_utils.parse_yaml_to_namespace(), which usesprimus/core/config/yaml_loader.parse_yaml()for${VAR}/${VAR:default}substitution andextends:inheritance with deep merge.- Per trainer module (names containing
trainer, for examplepre_trainer):PresetLoader.load()loads the module preset fromprimus/configs/modules/<framework>/<config>.yaml.PresetLoader.load()loads the model preset fromprimus/configs/models/<framework>/<model>.yaml.- Each preset is loaded through the same YAML pipeline (env substitution and
extends:chains).
parse_platform()merges platform settings fromprimus/configs/platforms/(defaulting toplatform_azure.yamlwhen the experiment omitsplatform).- CLI overrides: For
primus train,main()passesunknown_argsinto the train handler.PrimusRuntimeparses them withparse_cli_overrides()and deep-merges them intomodule_config.params. - Result: A resolved configuration where each module exposes a
paramsnamespace (SimpleNamespace) for training parameters, produced by_normalize_module_for_runtime()inprimus/core/config/primus_config.py.
The object returned from load_primus_config() is a lightweight SimpleNamespace (not PrimusConfig), with modules as a list of module configs, each tagged with a .name field.
4. Core runtime (PrimusRuntime)
primus/core/runtime/train_runtime.py defines PrimusRuntime, the main orchestrator for the new core training path. Execution for a single module follows this flow:
load_primus_config()loads and validates the experiment;get_module_config()selects the requested module (for examplepre_trainerorpost_trainer)._apply_overrides()merges CLI overrides intomodule_config.params._initialize_environment()ensures the data directory exists and callssetup_training_env()(Hugging Face cache and related setup)._initialize_distributed_context()reads torchrun-style rank and master information viaget_torchrun_env()._initialize_logging()initializes worker logging.BackendRegistry.get_adapter(framework)resolves theBackendAdapter(lazy-importingprimus.backends.<name>if needed).adapter.setup_backend_path()inserts the backend tree onsys.path. Resolution order: CLI--backend_path, then theBACKEND_PATHenv var, then the default—third_party/<dir>under the repo root, followed by$PRIMUS_THIRDPARTY_DIRor~/.cache/Primus/third_party(theprimus-cli deps synclocation).adapter.prepare_backend()runs backend setup hooks (viaBackendRegistry.run_setup()by default).adapter.convert_config(module_config.params)producesbackend_argsfor the trainer.run_patches(phase="build_args", ...)runs registered patches; backend version detection runs when patches first need it (adapter.detect_backend_version()via_get_backend_version()).merge_namespace()mergesbackend_argsintomodule_config.params(backend wins on conflicts);adapter.load_trainer_class(stage)resolves the trainer class (default stagepretrain).TrainerClass(backend_args=backend_args)constructs the trainer.run_patches(phase="setup")thentrainer.setup().trainer.init().run_patches(phase="before_train")thentrainer.train()thenrun_patches(phase="after_train")thentrainer.cleanup().- On failure,
_safe_cleanup()callstrainer.cleanup(on_error=True)when possible.
5. Backend system
BackendAdapter(primus/core/backend/backend_adapter.py) is the abstract integration surface. Subclasses implementconvert_config(),load_trainer_class(), anddetect_backend_version(). Shared behavior includessetup_backend_path()and a defaultprepare_backend()that runs registered setup hooks.BackendRegistry(primus/core/backend/backend_registry.py) maps backend names to adapter classes, supports lazy import ofprimus.backends.<backend>, and stores optional setup hooks per backend.- Registered adapters (via each backend package’s
__init__.pycallingBackendRegistry.register_adapter()):megatron,torchtitan,maxtext,megatron_bridge,hummingbirdxt. - Backend code lives under
primus/backends/<name>/. Importing the package registers the adapter and any trainers or hooks that package defines.
6. Trainer lifecycle
BaseTrainer(primus/core/trainer/base_trainer.py) defines the lifecycle.setup(),init(), andtrain()are abstract (subclasses must implement them);cleanup(on_error=False)is optional—it ships a default (no-op) implementation that subclasses may override. The constructor storesbackend_argsand reads distributed settings fromget_torchrun_env().- Concrete trainers (for example Megatron or TorchTitan pretrain classes) subclass
BaseTrainerand implement the abstract methods. PrimusRuntimedrivessetup→init→train→cleanup, with patch phasesbuild_args(before the trainer is created),setup, andbefore_train/after_train(aroundtrain). No patch phase runs aroundcleanup—after_trainfires beforecleanup(see §4).
7. Patch system
PatchRegistry(primus/core/patches/patch_registry.py) storesFunctionPatchobjects keyed by backend and phase, with wildcard buckets (None) for patches that apply broadly.- The
@register_patchdecorator registers a patch with metadata (priority, optional version patterns, tags). run_patches()(primus/core/patches/patch_runner.py) collects applicable patches, filters byPatchContext, sorts by priority, and runs handlers. It accepts an optionalenabled_idslist; if omitted, behavior is controlled byPRIMUS_PATCHES:- unset or
all: all patches none: disable all- comma-separated IDs: only those patches
- unset or
- Phases used by the core runtime include
build_args,setup,before_train, andafter_train. - Patch implementations are typically colocated with backends under
primus/backends/<backend>/patches/.
8. Legacy runtime
The legacy pretrain path—previously selected with PRIMUS_TRAIN_RUNTIME=legacy and backed by the primus/modules/ stack (BaseModule-style composition)—has been removed. primus/modules/ no longer contains any source code, and primus/cli/subcommands/train.py no longer reads PRIMUS_TRAIN_RUNTIME or resolves a legacy-vs-core runtime.
All training now runs exclusively through the core runtime: both primus train pretrain and primus train posttrain construct a PrimusRuntime (primus/core/runtime/train_runtime.py). primus/pretrain.py now only provides shared backend-path / environment helpers (for example setup_backend_path()) used by the training and projection entry points; it no longer defines a launch_pretrain_from_cli() legacy launcher.
9. Key source files
| Path | Role |
|---|---|
primus/cli/main.py | CLI entry, subcommand discovery, dispatch |
primus/cli/subcommands/train.py | train subcommand; chooses core vs legacy pretrain; posttrain via PrimusRuntime |
primus/core/launcher/parser.py | PrimusParser: experiment, platform, and module preset loading |
primus/core/config/preset_loader.py | PresetLoader: load framework presets from primus/configs/ |
primus/core/config/yaml_loader.py | YAML load with env substitution and extends |
primus/core/config/primus_config.py | load_primus_config(), get_module_config(), module normalization |
primus/core/runtime/train_runtime.py | PrimusRuntime, TrainContext |
primus/core/backend/backend_adapter.py | BackendAdapter ABC |
primus/core/backend/backend_registry.py | BackendRegistry |
primus/core/trainer/base_trainer.py | BaseTrainer ABC |
primus/core/patches/patch_registry.py | PatchRegistry, @register_patch |
primus/core/patches/patch_runner.py | run_patches(), PRIMUS_PATCHES parsing |
runner/primus-cli-*.sh | Shell wrappers for direct, container, and Slurm launch |
For a deep dive on the CLI internals (subcommand discovery, dispatch, and the launch wrappers), see CLI Architecture. For day-to-day contribution workflows (style, tests, CI), see Contributing Guide and Testing Guide.