πŸ—οΈ ArchitectureΒΆ

This page covers MMIRAGE’s internal module layout and the key design decisions behind each subsystem. It is aimed at contributors and developers who want to understand or modify the codebase.

If you are looking for a user-facing explanation of what happens when you run mmirage run, read Pipeline instead.


High-Level OverviewΒΆ

Each shard follows the same three-stage pipeline:

MMIRAGE pipeline β€” Loading Data β†’ Processor β†’ Write Data

At the orchestration level, the CLI manages shard dispatch, retry logic, and optional SLURM submission:

               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚                    mmirage CLI                       β”‚
               β”‚  run / submit / check / retry / merge / stats        β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚              launch_pipeline                β”‚
              β”‚  (local loop or SLURM array submission)     β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚  spawns
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚           shard_process.py                  β”‚
              β”‚  (one process per shard)                    β”‚
              β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚          β”‚              β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
        β”‚  Loader   β”‚  β”‚ Mapper  β”‚  β”‚ Renderer  β”‚
        β”‚(Dataset)  β”‚  β”‚(Compute)β”‚  β”‚(Jinja2)   β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                 β”‚          β”‚            β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚     shard_utils.py        β”‚
              β”‚  atomic save + state mgmt β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Package LayoutΒΆ

src/mmirage/
β”œβ”€β”€ __init__.py              Public API surface (MMirageConfig, load_mmirage_config …)
β”œβ”€β”€ cli.py                   CLI entry point and subcommand handlers
β”œβ”€β”€ shard_process.py         Single-shard processing script
β”œβ”€β”€ shard_utils.py           Shard state, atomic saves, GPU polling, benchmarking
β”œβ”€β”€ merge_shards.py          Post-processing: merge shard_* dirs into one dataset
β”‚
β”œβ”€β”€ config/                  Configuration layer (pure dataclasses, no heavy deps)
β”‚   β”œβ”€β”€ config.py            MMirageConfig, ExecutionParams, ProcessingParams
β”‚   β”œβ”€β”€ loading.py           LoadingParams, env-var resolution
β”‚   β”œβ”€β”€ batch_provider.py    Provider-neutral BatchProviderConfig
β”‚   β”œβ”€β”€ openai_batch.py      OpenAIBatchConfig (extends BatchProviderConfig)
β”‚   β”œβ”€β”€ anthropic_batch.py   AnthropicBatchConfig (extends BatchProviderConfig)
β”‚   └── utils.py             YAML loader, env-var expansion, dacite wiring
β”‚
β”œβ”€β”€ cli_utils/               CLI helpers
β”‚   β”œβ”€β”€ runtime.py           Path expansion, file logging, setup_runtime
β”‚   β”œβ”€β”€ slurm.py             sbatch script generation, job submission, polling
β”‚   └── status.py            Shard status reads, retry budget, check_failed_shards
β”‚
└── core/
    β”œβ”€β”€ loader/              Dataset loading
    β”‚   β”œβ”€β”€ base.py          BaseDataLoader, DataLoaderRegistry
    β”‚   β”œβ”€β”€ jsonl.py         JSONL loader (type: "JSONL")
    β”‚   β”œβ”€β”€ local_hf.py      HuggingFace load_from_disk (type: "loadable")
    β”‚   └── utils.py         load_datasets_from_configs helper
    β”‚
    β”œβ”€β”€ process/             Data transformation
    β”‚   β”œβ”€β”€ variables.py     InputVar, OutputVar, VariableEnvironment, JMESPath cache
    β”‚   β”œβ”€β”€ base.py          BaseProcessor, ProcessorRegistry, TokenCounts
    β”‚   β”œβ”€β”€ mapper.py        MMIRAGEMapper β€” orchestrates variables through processors
    β”‚   β”œβ”€β”€ processors/
    β”‚   β”‚   β”œβ”€β”€ llm/
    β”‚   β”‚   β”‚   β”œβ”€β”€ config.py              SGLangLLMConfig, SGLangServerArgs, LLMOutputVar
    β”‚   β”‚   β”‚   └── llm_processor.py       LLMProcessor β€” SGLang engine wrapper
    β”‚   β”‚   β”œβ”€β”€ custom/
    β”‚   β”‚   β”‚   β”œβ”€β”€ config.py              CustomProcessorConfig, CustomOutputVar
    β”‚   β”‚   β”‚   β”œβ”€β”€ custom_processor.py    CustomProcessor β€” pebble pool, circuit breaker
    β”‚   β”‚   β”‚   └── worker.py              Spawned-worker script loading and execution
    β”‚   β”‚   └── batch_api/
    β”‚   β”‚       β”œβ”€β”€ config.py              BatchApiProcessorConfig
    β”‚   β”‚       └── batch_api_processor.py BatchApiProcessor β€” provider batch submission
    β”‚   └── batch/           Async/batch inference subsystem
    β”‚       β”œβ”€β”€ orchestrator.py       End-to-end batch pipeline
    β”‚       β”œβ”€β”€ adapter.py            Provider-neutral batch adapter interface
    β”‚       β”œβ”€β”€ openai_adapter.py     OpenAI Batch API adapter
    β”‚       β”œβ”€β”€ anthropic_adapter.py  Anthropic Messages Batches adapter
    β”‚       β”œβ”€β”€ chunking.py           Request chunking (byte/count limits)
    β”‚       β”œβ”€β”€ collector.py          Response collection and result joining
    β”‚       β”œβ”€β”€ status_checker.py     Batch job polling
    β”‚       └── registry.py           Adapter registry
    β”‚
    └── writer/
        └── renderer.py      TemplateRenderer β€” Jinja2 output_schema rendering

Data FlowΒΆ

Single Shard (local mode)ΒΆ

  1. Config loading β€” load_mmirage_config reads the YAML, expands ${ENV_VAR} references, and constructs a typed MMirageConfig via dacite.

  2. Dataset loading β€” load_datasets_from_configs calls the appropriate DataLoader (JSONL or loadable), returning a HuggingFace Dataset.

  3. Sharding β€” The dataset is split into num_shards slices; this shard processes slice shard_id.

  4. Mapping β€” MMIRAGEMapper.rewrite_batch iterates over batches:

    • Extracts InputVar values from each sample using cached JMESPath expressions.

    • Resolves image inputs to PIL Images or absolute paths.

    • Calls the registered Processor (e.g. LLMProcessor) for each OutputVar.

  5. Rendering β€” TemplateRenderer.batch_render applies the output_schema Jinja2 templates, substituting variable values. Simple {{ var }} references bypass Jinja2 to preserve non-string types (e.g. PIL Images).

  6. Atomic save β€” The processed shard is written to shard_<id>/ under output_dir using a temp-then-rename pattern with hostname + PID + UUID to avoid cross-host collisions on shared filesystems.

  7. State marker β€” A status.json file is written to the state directory recording success or failure and the attempt count.

SLURM modeΒΆ

launch_pipeline generates and submits an sbatch array script. Each array task runs shard_process.py with SLURM_ARRAY_TASK_ID as the shard ID. The orchestrator polls job status via squeue, waits for the settle_time_seconds, checks status.json for each shard, and retries failed shards up to max_retries.

Batch API mode (OpenAI, Anthropic)ΒΆ

The BatchApiProcessor delegates request submission to the batch orchestrator:

  1. Requests are serialized to JSONL chunks respecting max_chunk_bytes / max_requests_per_chunk.

  2. Each chunk is uploaded/submitted and a metadata receipt is written.

  3. The mapper writes __BATCH_SUBMITTED__:<custom_id> placeholders into the output shards.

  4. Results are later checked/collected via mmirage.core.process.batch.status_checker and mmirage.core.process.batch.collector.


Key Design DecisionsΒΆ

Registry pattern for loaders and processorsΒΆ

Both DataLoaderRegistry and ProcessorRegistry use a decorator-based registry. New loaders/processors self-register at import time, keeping the core pipeline agnostic of concrete implementations.

Dacite + dataclasses for configΒΆ

All configuration is expressed as plain Python dataclasses. dacite converts the raw YAML dict into typed objects, providing structural validation without a heavy schema library at runtime.

JMESPath cachingΒΆ

Compiled JMESPath expressions are cached in a module-level dict to avoid recompilation on every sample β€” important for high-throughput processing.

Atomic shard savesΒΆ

Output shards are first written to a temporary directory with a host+PID+UUID suffix, then renamed. This guarantees crash-safe writes and avoids collisions on SLURM shared filesystems where multiple nodes may share a PID space.

Separation of config and heavy depsΒΆ

The config/ package has minimal imports (no torch, sglang, transformers). The core/process/processors/llm/config.py module is also lightweight β€” it registers the processor configuration without importing the SGLang engine. The engine is only imported when a shard actually processes data, enabling fast CLI startup and documentation builds.


See alsoΒΆ

  • Pipeline β€” user-facing walkthrough of the data flow

  • Concepts β€” vocabulary used throughout the codebase

  • Developer Guide β€” adding loaders and processors, running tests