Architecture overview

July 15, 2026 · View on GitHub

This document provides a detailed overview of the diffusion model architecture in Primus, including design decisions, directory structure, and implementation patterns.


Table of contents

  1. Design philosophy
  2. Directory structure
  3. Architectural decisions
  4. Component hierarchy
  5. Data flow
  6. Comparison with alternative implementations

Design philosophy

Core principles

  1. Megatron-Core Native: Built on Megatron-Core patterns and conventions
  2. Extensibility: Easy to add new models (DiT, MovieGen, custom)
  3. Clarity: Clear separation between shared and model-specific code
  4. Reusability: Shared components across multiple models
  5. Performance: Support for precalculated data and multi-GPU training

Architectural advantages

AspectPrimus Design Choice
Model Locationcore/models/diffusion/ (Megatron-Core convention)
Layer OrganizationUnified TransformerBlock with heterogeneous specs
Shared CodeDedicated common/ directory
EncodersHierarchical encoders/{type}/{variant}/
EnergonShared data/energon/ for all models
Mock DataSynthetic providers under data/synthetic/
FrameworkPure Megatron (no PyTorch Lightning dependency)

Directory structure

High-level organization

primus/backends/megatron/
├── core/models/diffusion/          # Core model implementations
├── training/diffusion/             # Training utilities (schedulers, etc.)
└── data/
    ├── energon/                    # Shared Energon infrastructure
    └── diffusion/                  # Diffusion-specific data

Detailed breakdown

1. Core models (core/models/diffusion/)

Following Megatron-Core convention (megatron/core/models/gpt/, megatron/core/models/multimodal/):

core/models/diffusion/
├── common/                         # Shared building blocks
│   ├── __init__.py
│   ├── config.py                   # BaseDiffusionConfig
│   ├── embeddings.py               # TimeStepEmbedder, Timesteps, MLPEmbedder
│   └── normalization.py            # RMSNorm, AdaLN, AdaLNContinuous

└── flux/                           # Flux-specific components
    ├── __init__.py
    ├── config.py                   # FluxConfig
    ├── model.py                    # Flux (main model class)
    ├── layer_spec.py               # MMDiTLayer, FluxSingleTransformerBlock
    ├── attention.py                # JointSelfAttention, FluxSingleAttention
    ├── layers.py                   # EmbedND (RoPE), embedders
    ├── checkpoint_converter.py     # HF <-> Megatron conversion
    └── utils.py                    # image position ids, helpers

Rationale:

  • common/: shared building blocks (base config, timestep/positional embeddings, normalization) reusable across diffusion models
  • flux/: Flux-specific code (model class, MMDiT and single-block layer specs, joint attention, RoPE embedders)
  • Clear separation keeps model-specific code isolated and shared code reusable for future models

2. Training (training/diffusion/)

training/diffusion/
├── __init__.py
└── schedulers/
    ├── __init__.py
    ├── base.py                     # BaseScheduler
    ├── flow_matching.py            # FlowMatchEulerDiscreteScheduler
    ├── ddpm.py                     # [Future] DDPMScheduler
    ├── edm.py                      # [Future] EDMScheduler
    └── euler.py                    # [Future] EulerDiscreteScheduler

Rationale:

  • Schedulers define noise schedules and training targets
  • Separate from model code for clarity
  • Easy to add new schedulers (DDPM, EDM, etc.)

3. Data pipeline (data/)

data/
├── energon/                        # Shared Energon utilities
│   └── __init__.py
├── dataloader.py                   # MegatronDataloaderWrapper (wraps any iterable)

└── diffusion/
    ├── __init__.py
    ├── encoders/                   # Hierarchical encoder registry
    │   ├── __init__.py
    │   ├── registry.py             # EncoderRegistry
    │   ├── image/
    │   │   └── vae/
    │   │       ├── __init__.py
    │   │       ├── autoencoder_kl.py   # AutoencoderKL
    │   │       └── vqvae.py            # VQVAE
    │   └── text/
    │       ├── t5/
    │       │   ├── __init__.py
    │       │   ├── t5_xxl.py           # T5-XXL
    │       │   └── t5_large.py         # T5-Large
    │       └── clip/
    │           ├── __init__.py
    │           ├── clip_l.py           # CLIP-L
    │           └── clip_h.py           # CLIP-H

    ├── preprocessing/
    │   ├── __init__.py
    │   └── image/
    │       ├── __init__.py
    │       ├── transforms.py           # Resizing, normalization
    │       └── augmentation.py         # Data augmentation

    └── task_encoders/
        ├── __init__.py
        └── image.py  # EncodedDiffusionTaskEncoder

Rationale:

  • Energon shared: VLM and other models can reuse Energon utilities
  • Hierarchical encoders: Organized by modality and variant type
  • Registry pattern: Config-driven encoder selection
  • Preprocessing separated: Clear pipeline stages

Architectural decisions

Decision 1: Models under core/models/

Choice: primus/backends/megatron/core/models/diffusion/ Not: primus/backends/megatron/models/diffusion/

Reasoning:

  • Aligns with Megatron-Core structure
  • Easier upstream tracking
  • Clear that these are Megatron-Core compatible
  • Consistent with existing Primus structure (core/models/gpt/)

Decision 2: Separate common/ directory

Choice: Shared components in common/ Not: Everything in flux/ or flat structure

Reasoning:

  • Standard approach puts shared code in model-specific directories
  • Primus: common/ makes it explicit what's shared
  • Future DiT implementation trivial (reuse from common/)
  • Clear contract: if in common/, must work for all models

Shared Components:

  • JointSelfAttention: Used by DiT and Flux joint layers
  • FluxSingleAttention: Used by DiT and Flux single layers
  • MMDiTLayer: Joint (multimodal) transformer block
  • FluxSingleTransformerBlock: Single-modality transformer block

Flux-Only Components:

  • EmbedND: 3D RoPE position embedding (Flux-specific)
  • Flux model class

Decision 3: Hierarchical encoder structure

Choice: encoders/image/vae/, encoders/text/t5/, encoders/text/clip/ Not: Flat encoders/conditioner.py

Reasoning:

  • Traditional approach uses flat structure with all encoders in one file
  • User requirement: support 5+ variants per modality
  • Registry pattern enables config-driven selection
  • Easy to add new encoders without modifying existing files

Registry Pattern:

from primus.backends.megatron.data.diffusion.encoders import get_encoder

# Config-driven selection
vae = get_encoder('autoencoder_kl', config=vae_config)
t5 = get_encoder('t5_xxl', config=t5_config)
clip = get_encoder('clip_l', config=clip_config)

Decision 4: Shared Energon infrastructure

Choice: data/energon/ for shared utilities Not: Nested under data/diffusion/

Reasoning:

  • Energon is general-purpose (VLM, diffusion, future models)
  • Traditional approach nests under model-specific directories
  • Megatron-LM has Energon at example level (not in core)
  • Primus approach: shared infra + model-specific TaskEncoders

Pattern:

# Shared: data/dataloader.py
# MegatronDataloaderWrapper wraps any iterable (Energon loader, PyTorch DataLoader, etc.)
wrapper = MegatronDataloaderWrapper(dataloader)

# Model-specific: data/diffusion/task_encoders/image.py
class EncodedDiffusionTaskEncoder:
    """Diffusion-specific encoding logic"""
    pass

Decision 5: Model provider at adapter level

Choice: construct the model through provider functions in the Megatron trainer/adapter layer under primus/backends/megatron/, above core/models/.

Reasoning:

  • Model providers are adapter/wrapper functions rather than a standalone module; they live alongside the trainers (for example primus/backends/megatron/megatron_pretrain_trainer.py)
  • They sit above the core models to add Primus-specific functionality
  • Example: wrap the model with a custom loss, precision handling, or checkpoint logic

Decision 6: No PyTorch lightning

Choice: Pure Megatron patterns Not: PyTorch Lightning DataModules

Reasoning:

  • Primus doesn't use PyTorch Lightning
  • Framework-specific implementations reduce flexibility
  • Better integration with Megatron training loop
  • Follows Megatron-LM's MegatronDataloaderWrapper wrapper pattern

Component hierarchy

1. Model hierarchy

nn.Module (PyTorch)
└── MegatronModule
    └── DiffusionModule (abstract)
        └── Flux (concrete)
        ├── Joint layers: MMDiTLayer × num_joint_layers
        │   └── JointSelfAttention (shared)
        ├── Single layers: FluxSingleTransformerBlock × num_single_layers
        │   └── FluxSingleAttention (shared)
        └── Embeddings:
            ├── TimeStepEmbedder (shared)
            ├── MLPEmbedder (shared)
            └── EmbedND (Flux-specific, 3D RoPE)

2. Configuration hierarchy

TransformerConfig (Megatron-Core)
└── BaseDiffusionConfig
    └── FluxConfig
        ├── flux_535m() factory
        └── flux_12b() factory

3. Scheduler hierarchy

BaseScheduler (abstract)
├── FlowMatchEulerDiscreteScheduler (Flux)
├── DDPMScheduler (future)
├── EDMScheduler (future)
└── EulerDiscreteScheduler (future)

4. Encoder hierarchy

BaseEncoder (abstract)
├── ImageEncoder
│   └── VAE
│       ├── AutoencoderKL (Flux)
│       └── VQVAE (future)
└── TextEncoder
    ├── T5
    │   ├── T5-XXL (Flux)
    │   └── T5-Large (future)
    └── CLIP
        ├── CLIP-L (Flux)
        └── CLIP-H (future)

Data flow

Training pipeline

Raw Data (images + captions)

[Optional] Precalculation
    ├─ VAE → latents [B, 64, H/8, W/8]
    ├─ T5-XXL → embeddings [B, 512, 4096]
    └─ CLIP-L → pooled [B, 768]

WebDataset/Energon Format (.tar files)

MegatronDataloaderWrapper (from data/dataloader.py)

EncodedDiffusionTaskEncoder (from data/diffusion/task_encoders/)
    ├─ Load precalculated data, OR
    └─ Encode on-the-fly (slower)

Training Batch:
    ├─ latents: [B, 64, H, W]
    ├─ t5_embeddings: [B, S, 4096]
    └─ clip_pooled: [B, 768]

FlowMatchEulerDiscreteScheduler
    ├─ Sample timesteps: t ~ U(0, 1)
    ├─ Sample noise: ε ~ N(0, I)
    ├─ Add noise: x_t = (1-t)*ε + t*x_0
    └─ Compute target: v = x_0 - ε

Flux Model Forward Pass
    ├─ Embed timesteps
    ├─ Embed pooled text (CLIP)
    ├─ Joint layers (process latents + T5 embeddings)
    ├─ Single layers (process latents only)
    └─ Output: v_pred [B, 64, H, W]

Loss Computation: MSE(v_pred, v_target)

Backward Pass & Optimizer Step

Inference pipeline

Text Prompt

Text Encoders
    ├─ T5-XXL → embeddings [1, S, 4096]
    └─ CLIP-L → pooled [1, 768]

Initialize Noise: x_0 ~ N(0, I)

Sampling Loop (t = 1.0 → 0.0)
    ├─ Model forward: v_t = Flux(x_t, t, embeddings)
    ├─ Update: x_{t-dt} = x_t + v_t * dt
    └─ Repeat until t = 0

Latents: x_0 [1, 64, H, W]

VAE Decoder

Generated Image [1, 3, H*8, W*8]

Comparison with alternative implementations

Primus architectural advantages

1. TransformerBlock architecture (Primus innovation)

  • Primus: Unified TransformerBlock with heterogeneous layer specs
  • Others: Separate nn.ModuleList containers for double/single blocks
  • Benefit: Better PP slicing, unified checkpointing, future-proof

2. Megatron-core native

  • Primus: Pure Megatron-Core, no framework dependencies
  • Others: Often integrated with PyTorch Lightning or other frameworks
  • Benefit: Tighter integration, simpler training loops

3. Checkpoint format

  • Primus: Unified transformer.layers.{0-56} structure
  • Others: Separate double_blocks.{i} and single_blocks.{j}
  • Benefit: Simpler distributed checkpointing

4. Encoder architecture

  • Primus: Registry-based, hierarchical organization
  • Others: Direct imports from monolithic files
  • Benefit: Easy extensibility for new encoder variants

File organization comparison

ComponentStandard LocationPrimus LocationImprovement
Modelmodels/diffusion/flux/core/models/diffusion/flux/Megatron-Core convention
LayersMixed locationscommon/ for shared, flux/ for specificClear boundaries
EncodersSingle filedata/diffusion/encoders/{type}/{variant}/Hierarchical, extensible
TestsMixed with codetests/unit_tests/backends/megatron/diffusion/Proper separation

Implementation status

Core infrastructure ✅

  • ✅ Directory structure
  • ✅ Base classes (DiffusionModule, BaseDiffusionConfig, BaseScheduler)
  • ✅ DiffusionModule with Megatron-Core integration
  • ✅ FluxConfig with factory methods
  • ✅ FlowMatchEulerDiscreteScheduler implementation
  • ✅ Configuration files (YAML)
  • ✅ Testing framework (290+ tests)
  • ✅ Documentation structure

Flux model implementation ✅

  • ✅ Flux model architecture
  • ✅ MMDiT layers and attention
  • ✅ Embeddings (RoPE, timestep, vector)
  • ✅ Encoder registry and loaders
  • ✅ TaskEncoder for Energon
  • ✅ Data pipeline

Extension points

Adding a new model (e.g., DiT)

  1. Create model directory: core/models/diffusion/dit/
  2. Add config: Extend BaseDiffusionConfig
  3. Implement model: Extend DiffusionModule (which extends MegatronModule)
    • Inherit process group management
    • Get distributed checkpointing support
    • Access attention backend configuration
  4. Reuse shared components: Import from common/
  5. Add tests: tests/unit_tests/backends/megatron/diffusion/test_dit_model.py
  6. Update configs: Add dit_config.yaml

Adding a new encoder variant

  1. Create encoder file: e.g., data/diffusion/encoders/text/t5/t5_large.py
  2. Implement encoder class: Extend BaseEncoder
  3. Register: Add to ENCODER_REGISTRY
  4. Add config: Update encoders.yaml
  5. Add tests: Test in tests/unit_tests/backends/megatron/diffusion/data/encoders/

Adding a new scheduler

  1. Create scheduler file: training/diffusion/schedulers/ddpm.py
  2. Implement: Extend BaseScheduler
  3. Export: Add to __init__.py
  4. Add tests: tests/unit_tests/backends/megatron/diffusion/training/test_scheduler.py
  5. Document: Update this file and README

Performance considerations

Memory optimization

  • Precalculated data: 5-10x faster, lower memory
  • Frozen encoders: Only train diffusion model
  • Gradient checkpointing: Trade compute for memory
  • Mixed precision: bf16 on MI300X (compatible with H100/A100)

Multi-GPU scaling

  • Tensor Parallelism: Split model across GPUs
  • Pipeline Parallelism: Split layers across GPUs
  • Data Parallelism: Replicate model, split data
  • Sequence Parallelism: For very long sequences

Best practices

  1. Use precalculated mode for training
  2. Freeze encoders (standard practice)
  3. Use bf16 on modern hardware
  4. Start with TP=1, PP=1, scale as needed
  5. Profile before optimizing

References


Last Updated: December 2025