A lightweight language model pretraining / continued-pretraining / supervised fine-tuning framework built on vanilla PyTorch and HuggingFace Accelerate for the (continued-)pretraining loop, and TRL's SFTTrainer for the SFT loop. Configuration is driven by Hydra, experiment tracking by trackio.
- Vanilla PyTorch: Pure PyTorch pretraining loop without heavy frameworks
- HuggingFace Accelerate: Seamless distributed training support (multi-GPU, mixed precision)
- Hydra Configuration: YAML-based configuration with composable model presets and CLI overrides
- Continued Pretraining (CPT): Resume/continue training from any checkpoint or pretrained model directory, optionally with a LoRA adapter
- Supervised Fine-Tuning (SFT): TRL-based SFT with conversational data and assistant-only loss
- LoRA / PEFT: Train adapters instead of full parameters for both CPT and SFT (via
peft) - Adapter Merging: Merge a trained LoRA adapter back into its base model to bridge CPT → SFT
- Mixed Precision Training: Built-in bf16 support for faster training
- Gradient Accumulation: Efficient training with large effective batch sizes
- Dataset Packing: Efficient sequence packing to maximize token utilization
- Crash-Safe Resume: Full training-state checkpointing (step/epoch/tokens seen + dataloader position)
- Token-Budget Stopping: Stop training after a target number of tokens (
total_tokens) - Automatic Logging: GPU metrics and training stats via trackio, plus a write-enabled dashboard
- Custom Tokenizers: Support for custom tokenizer training and usage
- Checkpoint Management: Top-K checkpoint saving based on validation loss (plus a fixed
last/) - Evaluation Benchmarks: Built-in support for HumanEval, IFEVAL, and MMLU
The framework supports two ways of obtaining a model, and is architecture-agnostic on the load path:
When no saved_checkpoint_path is given, the model is built from a size preset under configs/model/. These presets currently target the Qwen3 architecture:
| Preset | Layers | Hidden Size | FFN Size | Head Dim | Heads |
|---|---|---|---|---|---|
qwen_tiny |
4 | 768 | 1024 | 64 | 4 |
qwen_small |
8 | 768 | 1024 | 64 | 8 |
qwen_medium |
15 | 768 | 1024 | 64 | 8 |
qwen_large |
20 | 768 | 1024 | 128 | 8 |
qwen_xlarge |
20 | 768 | 1024 | 64 | 8 |
All presets use Qwen/Qwen3-0.6B as the base model config reference and support customizable vocabulary size via custom tokenizers.
When saved_checkpoint_path (training) or model_name_or_path (SFT) points at a checkpoint or a saved model directory, the model is loaded via AutoConfig/AutoModelForCausalLM, so any HuggingFace causal-LM architecture works — the size presets are bypassed.
Training is configured via Hydra YAML files. The main (continued-)pretraining config is configs/train.yaml, which composes a model preset via defaults. Settings are grouped into per-concern sections (data, optimizer, scheduler, accelerate, validation, logging, checkpoint, evaluation, and an optional lora), with run-level fields kept at the top level:
defaults:
- model: qwen_small # or qwen_tiny, qwen_medium, qwen_large, qwen_xlarge
- _self_
# Run-level (top-level)
saved_checkpoint_path: null # continue from a checkpoint / pretrained dir; null = build from preset
compile: "torch" # torch.compile; set null (recommended with LoRA)
num_epochs: 1
total_steps: null
total_tokens: null # token-budget stop (highest priority when set)
# LoRA adapter (optional). Omit / null for full-parameter training.
lora:
r: 32
lora_alpha: 32
lora_dropout: 0.05
target_modules: ["q_proj", "v_proj"] # null lets peft auto-infer
# Data
data:
tokenizer_path: "./models/lfm2_5_base/"
max_seq_length: 2048
use_packed_data: true
num_workers: 4
train_batch_size: 8
val_batch_size: 8
# Optimizer (incl. gradient clipping)
optimizer:
learning_rate: 8e-5
weight_decay: 0.01
betas: [0.9, 0.95]
eps: 1e-10
max_grad_norm: 1.0
# Scheduler
scheduler:
warmup_steps: 1000
# Accelerate
accelerate:
mixed_precision: "bf16"
gradient_accumulation_steps: 1
# Validation
validation:
val_check_interval: 4000 # steps
val_size: 80000 # held-out validation samples
# Logging
logging:
project_name: "my-cpt-run"
auto_log_gpu: true
log_every_n: 10
# Checkpointing
checkpoint:
save_dir: "checkpoints"
experiment_name: null # subdir under save_dir; falls back to model.name when null
run_name: null # nested run subdir; defaults to a timestamp when null
save_top_k: 1
save_every_n_steps: 4000
save_last: true
max_shard_size: "5GB"Override any parameter from the command line using its section path:
python main.py data.train_batch_size=32 optimizer.learning_rate=3e-4 model=qwen_mediumDownload raw parquet data from the Hub (writes per-dataset files under data/) with the CLI:
uv run pretrain-data download <repo_id> <local_dir>Sample and mix the downloaded datasets into a single parquet file (data/mixed_dataset/data.parquet). Per-dataset row counts are set in the DATASET_WEIGHTS dict inside the script:
python concat_data.pyTokenize the parquet dataset into train/test splits using the pretrain-data CLI:
uv run pretrain-data tokenize \
--tokenizer-repo-id <tokenizer-id-or-path> \
--data-path data/mixed_dataset/data.parquet \
--output-path tokenized_data/Use --test-size to change the held-out fraction (default 0.1).
Pack tokenized sequences into fixed-length blocks for efficient training (eliminates padding waste):
python pack_data.py --max-seq-length 2048 \
--input-dir tokenized_data/train \
--output-dir tokenized_data/packed_train_data_2048Run (continued-)pretraining with your configuration:
# Single GPU
python main.py
# Multi-GPU with Accelerate
accelerate launch main.py
# With config overrides
python main.py model=qwen_large optimizer.learning_rate=3e-4
# Configure accelerate (first time)
accelerate configHelper scripts train.sh and launch.sh wrap accelerate launch main.py (setting CUDA_VISIBLE_DEVICES / activating the venv).
Set saved_checkpoint_path to a checkpoint or pretrained model directory to continue training an existing model instead of building one from a preset:
python main.py \
saved_checkpoint_path=./checkpoints/my_experiment/my_run/last \
total_tokens=1_000_000_000- Add a
lorasection to train a LoRA adapter on top of the frozen base (recommended for large-model CPT). With LoRA, only the adapter is saved into each checkpoint. - When resuming an interrupted run (a checkpoint that contains a
state/directory), the full training state — global step, epoch, tokens seen, and dataloader position — is restored automatically so no samples are seen twice or skipped. - When using LoRA, set
compile: null.
# Launch on all available GPUs
accelerate launch --multi_gpu main.py
# Launch on a specific number of GPUs
accelerate launch --num_processes 4 main.pyCPT checkpoints trained with LoRA store only the adapter. Before SFT-ing from such a checkpoint, merge the adapter into its base to produce a standalone model directory:
python merge_adapter.py \
--adapter-path checkpoints/my_experiment/my_run/last \
--output-dir models/my_cpt_mergedThe base model is read from the adapter's adapter_config.json unless --base-model is given explicitly. The merged directory (with tokenizer) can then be loaded by sft.py via model_name_or_path.
SFT is configured via configs/sft.yaml and run through TRL's SFTTrainer:
# Single GPU
python sft.py
# Multi-GPU with Accelerate
accelerate launch sft.pyKey configuration:
model_name_or_path: the merged-CPT directory (or any base model) to fine-tune.dataset:dataset_id(HF hub id or localsave_to_diskdir), splits, anddataset_format— one ofconversational(rows of{"messages": [...]}),prompt_completion, ortext.chat_template_path: a ChatML template with{% generation %}markers (configs/lfm2_chatml_gen.jinja) so assistant-only loss works on conversational data; set tonullto keep the tokenizer's own template.sft: knobs forwarded totrl.SFTConfig(output_dir,learning_rate,per_device_train_batch_size,max_length,assistant_only_loss,packing, eval/save intervals,bf16, …).lora: optional LoRA section for adapter-based SFT (omit for full-parameter).
trackio logging is wired automatically through a custom callback (pretrain.sft.task.TrackioCallback) — no report_to setting is needed.
Training uses bfloat16 by default for faster computation and lower memory usage.
Simulate larger batch sizes without OOM errors:
accelerate:
gradient_accumulation_steps: 4 # Effective batch size = data.train_batch_size * 4Linear warmup scheduler for a stable training start:
scheduler:
warmup_steps: 1000Automatic validation runs during training:
validation:
val_check_interval: 4000 # Run validation every 4000 stepsCombines multiple examples into fixed-length sequences to achieve near 100% token utilization (no padding waste). Enable with use_packed_data: true.
Add a lora section to train a LoRA adapter (via HuggingFace peft) instead of full-parameter training, for both continued pretraining (train.yaml) and SFT (sft.yaml). When lora is absent (the default), training is full-parameter.
lora:
r: 32
lora_alpha: 32
lora_dropout: 0.05
target_modules: ["q_proj", "v_proj"] # default; null lets peft auto-infer insteadFor CPT, only the adapter is trained (the base is frozen) and each checkpoint stores the adapter separately — adapter_config.json + adapter_model.safetensors, not a full model copy. The base model is taken from saved_checkpoint_path. When using LoRA it's recommended to set compile: null.
When a run is interrupted, resuming from a checkpoint that contains a state/ directory restores the full training state (step, epoch, tokens seen) and the dataloader position, so training continues exactly where it left off.
Automatically saves the top-K best checkpoints based on validation loss (older/worse ones are pruned), plus a fixed last/ checkpoint when save_last: true. Checkpoints are organized as checkpoints/<experiment_name>/<run_name>/....
Automatic GPU utilization, memory, and power logging via trackio.
The framework includes built-in evaluation benchmarks, configurable in the training YAML config (evaluation section, enabled master switch off by default):
- HumanEval: Code generation benchmark
- IFEVAL: Instruction-following evaluation
- MMLU: Multiple-choice knowledge assessment
Each benchmark supports configurable sample count, temperature, and max generation tokens. Results are saved to eval_results/.
Training metrics are logged using trackio, which provides:
- Training loss, learning rate, tokens processed
- Validation loss at regular intervals
- GPU utilization, memory usage, and power consumption
- Web-based dashboard for visualization
Metrics logged:
train_loss: Training loss per batchval_loss: Validation losslearning_rate: Current learning ratetokens_passed: Total tokens processed- GPU metrics (when
auto_log_gpu: true)
To open a write-enabled dashboard (so you can delete/rename runs from the browser):
uv run dashboard.py # all projects
uv run dashboard.py <project> # open a specific projectThe project expects tokenized datasets in HuggingFace Datasets format with:
input_ids: Tokenized sequences- Train/test splits saved by
pretrain-data tokenizeundertokenized_data/trainandtokenized_data/test - Packed data stored at
tokenized_data/packed_train_data_<max_seq_length>(e.g...._2048), selected automatically whenuse_packed_data: true
Defaults are config-driven; the values below reflect the current configs/train.yaml:
- Optimizer: AdamW with betas=(0.9, 0.95), weight_decay=0.01, eps=1e-10
- Scheduler: Linear warmup (
scheduler.warmup_steps) - Gradient Clipping: Max norm of 1.0
- Sequence Length: Configurable via
data.max_seq_length(default 2048)
- Python >= 3.12
- Managed with
uv(uv.lockcommitted). Key dependencies:accelerate,transformers,trl,peft,datasets,hydra-core,trackio,polars.
uv run pytestThe test suite (under tests/) is CPU-only and focused on the crash-resume correctness logic — training-state round-trips and deterministic dataloader skip-on-resume.