Architecture Overview
This document is the entry point to the LLenergyMeasure architecture documentation suite. It introduces the engine-knowledge subsystem - how llem learns each engine's configuration surface and validation rules - and shows how that connects to the runtime measurement framework.
Start here. Deep-dive docs for each subsystem are linked throughout.
Who this is for
- Engine extenders adding a new engine: read this overview, then engine extensibility for the plugin contract and CI architecture for what CI checks.
- Researchers: read this overview, then comparison context for how results relate to other benchmarks.
The two engine-knowledge products
For each engine, llem ships two committed artifacts that together let it validate a user's config before spending GPU time:
- A typed config schema.
schema.discovered.jsonrecords the engine's full parameter surface (fields, types, bounds, enums), discovered by introspecting the engine at a pinned version. A code generator turns it into the typed Pydantic modelsrc/llenergymeasure/config/generated/<engine>.pythat users configure against. - Validation rules.
rules.yamlis a list of validation rules - cross-field constraints, silent-normalisation cases - extracted from the engine's own source and verified against it. The runtime loader evaluates these against each submitted config.
Both artifacts are produced locally by a maintainer when an engine version bumps, committed to the repo, and shipped inside the wheel. CI never produces them; it only verifies that the committed bytes stay internally consistent (see CI architecture).
Runtime config validation
At runtime, when a user submits an ExperimentConfig, _apply_rules in src/llenergymeasure/config/models.py loads the engine's rules and evaluates each one against the config:
EngineRulesLoader().load_rules(<engine>)parses the shippedrules.yamlinto anEngineRulesobject.- For each
Rule,rule.try_match(config)returns aRuleMatchwhen the rule's predicate fires, orNonewhen it does not. - A matched rule at
errorseverity raises before engine initialisation begins (Pydantic surfaces it as aValidationError), so an invalid combination costs milliseconds rather than the minutes an engine takes to load weights and initialise CUDA. - A matched rule at
dormantseverity records that the engine will silently ignore or coerce a field. The study planner uses this to deduplicate configs that look distinct but resolve to the same effective configuration, so the GPU only runs cells that produce distinct measurements.
Severity is a closed two-value enum - error and dormant. There is no warn level: the study workflow records effective parameters separately, so a rule either changes what runs or it does not.
Deep dive: parameter discovery and config validation.
Keeping knowledge current
Upstream engines ship frequently. A self-hosted Renovate cron (renovate.yml) watches upstream releases and opens a pull request that bumps the pin in engine_versions/<engine>/current.yaml. Detection stays automatic; refreshing the committed knowledge is a local maintainer step on that PR:
- Re-discover the schema:
make discover-schema ENGINE=<engine>. - Re-absorb the rules against the new source:
make absorb ENGINE=<engine> SRC=<engine-source>. - Commit the regenerated
schema.discovered.json,config/generated/<engine>.py, andrules.yaml. - CI verifies the committed bytes are consistent before merge.
This is a deliberate split: production needs the engine source (and sometimes a GPU), so it runs on a maintainer's machine; verification is a fast read-only check that runs on hosted CPU runners. See CI architecture for the verification surface and schema refresh for the schema-side operations guide.
Broader framework context
The engine-knowledge subsystem plugs into Layer 0 (config/), which the rest of the stack builds on. Every ExperimentConfig constructed by the API or CLI passes through config/engine_rules/loader.py before reaching the harness.
Why validate before engine initialisation?
GPU time is the scarce resource. Two distinct failure modes burn it, and the two rule severities target them:
Duplicate runs from silent normalisation. Engines silently normalise many fields - seed=-1 becomes None, sampling parameters are dropped under greedy decoding. A sweep that varies such a field generates configs that look distinct to the user (and to Pydantic) but produce identical effective configurations once the engine has normalised them. dormant rules let the loader resolve the effective config at parse time so the study planner deduplicates measurement-equivalent cells; the GPU only runs cells that produce distinct measurements.
Invalid-combination late rejection. Engine initialisation is expensive: weights load from disk, CUDA contexts initialise, and TensorRT-LLM may compile an engine. A config rejected after two minutes of initialisation wastes that GPU time outright. error rules catch the most common cross-field violations at config-parse time.
The rules complement, rather than replace, engine-side validation: they capture the constraints that fire only in specific field combinations and the silent normalisations the engine does not warn about.
Why committed artifacts instead of live introspection?
Live introspection at runtime would require importing each engine at startup - which on vLLM and TensorRT-LLM means initialising CUDA contexts. The committed rules.yaml and schema.discovered.json load in a few milliseconds with no GPU dependency.
The trade-off is staleness risk: the artifacts must be refreshed when the engine library changes. The Renovate-driven bump PR plus the local refresh step and the CI byte-verification together enforce that discipline.
Key concepts
| Term | Meaning |
|---|---|
| Schema discovery | Introspecting an engine at its pinned version to record its full parameter surface as schema.discovered.json |
| Config codegen | Generating the typed Pydantic config/generated/<engine>.py from the discovered schema; CI asserts the regenerated file is byte-identical to the committed one |
| Rule | A single validation constraint in rules.yaml: an id, a severity, a match predicate over config fields, a message template, and provenance |
| Severity | Closed enum: error (reject the config) or dormant (the engine silently normalises a field; used for deduplication) |
| Absorb | The local workflow that reads an engine's source into candidate rules, verifies them against the engine, and promotes the confirmed ones into rules.yaml |
| Rules coverage | An advisory report of validator sites in the engine source that no shipped rule covers |
| Loader grammar | The predicate language used in match.fields: in, not_in, @field_ref, not_divisible_by, type_is, and so on |
File and package map
src/llenergymeasure/engines/
└── <engine>/ Per-engine sub-package, ships with the wheel
├── plugin.py EnginePlugin implementation (inference code)
├── schema.discovered.json Discovered parameter surface (pinned version)
└── rules.yaml Shipped validation rules (single runtime source)
src/llenergymeasure/config/
├── generated/
│ └── <engine>.py Typed Pydantic model, generated from the schema
└── engine_rules/
├── loader.py Runtime rule loader + predicate engine
└── __init__.py
engine_versions/
└── <engine>/current.yaml Per-engine pin: library version. Renovate-writable.
scripts/ Local production tooling (not shipped in the wheel)
├── absorb.py The absorb workflow entry point
├── analyst_cold_read.py Reads engine source into candidate rules
├── check_citations.py Confirms each candidate's citation resolves
├── probe_candidates.py Verifies candidates against the real engine
└── rules_coverage.py Advisory uncovered-validator-site report
See also
- Parameter discovery and config validation - the runtime loader
- Pipeline architecture - per-engine ordering and the asymmetric image architecture
- CI architecture - what CI verifies
- Engine extensibility - what a new engine must contribute
- Comparison context - how results relate to other benchmarks
- Engine configuration reference - the per-engine configuration surface
- Schema refresh - schema-side operations guide