If you have ever typed pip install optimum[onnxruntime] and watched a PyTorch checkpoint turn into an ONNX Runtime model with one class swap, you have met Optimum at its best. Optimum is HuggingFace’s optimization toolbox that sits beside Transformers as an extension of Transformers, Diffusers, timm and Sentence-Transformers, and its job is simple to state and hard to do well: take the same model you already loaded and run it faster on whatever hardware you actually own, whether that is a CPU, a GPU, an Intel NPU, an AWS Inferentia chip or a Habana Gaudi. Under the hood it is a large family of exporters, quantizers and optimized model wrappers — the ONNX Runtime ones being the oldest and best documented, which is why the ORTModelForXXX classes remain the reference point for the whole design.

The repository has quietly reinvented itself in the last couple of years. What used to be one giant package holding every exporter has been carved into optimum-* sibling distributions — optimum-onnx, optimum-onnxruntime, optimum-intel, optimum-neuron, optimum-habana and more — while the core optimum package keeps the shared contracts: the task router, the exporter configuration base classes, the optimized model base class, the quantizer interface, a full GPTQ implementation and a torch.fx toolkit. The CLI discovers installed backends at runtime through Python namespace packages, which is why the same optimum-cli binary grows subcommands as you install extras. This tour reads the core at commit 0df84f2 on main, mapping how a model travels from from_pretrained to a hardware-specific artifact.

Optimum architecture

The Optimum core: the CLI dispatches through subpackage discovery into the task router, export contract, model base and quantization stack.

Reading the overview from left to right:

  • main, L188 — the optimum-cli entry point; it registers root subcommands, calls load_subpackages(), then parses argv and runs whatever service was bound to the chosen subcommand.
  • ExportCommand, L20 — the export subcommand, a thin shell that hands model-plus-task resolution to each backend’s exporter CLI.
  • load_subpackages, L72 — scans installed distributions whose names start with optimum- and boots their subpackage modules so backends can register their own CLI commands.
  • TasksManager, L118 — the routing brain: task-name-to-model-class mappings for transformers, diffusers, timm and sentence-transformers, plus inference of task, library and framework.
  • get_exporter_config_constructor, L1236 — resolves which exporter config class applies to a given model type and task combination.
  • ExporterConfig, L77 — the base contract every backend config inherits: a normalized config class, dummy input generator classes and the absolute tolerance used to validate converted outputs.
  • OptimizedModel, L93 — the base class of ORTModelForXXX, OVModelForXXX and friends, with from_pretrained(export=True) doing on-the-fly conversion.
  • OptimumQuantizer, L10 — the two-method quantizer contract (from_pretrained + quantize) that ORTQuantizer, OVQuantizer and the GPTQ quantizer all implement.
  • Transformation, L85 — the reversible torch.fx rewrite primitive behind graph optimization and tensor parallelism.

One Core, Many Accelerators

The most consequential file in the repo is also one of the smallest. optimum/subpackages.py defines load_namespace_modules (L30), which iterates over installed distributions, filters for names starting with optimum-, explicitly skips optimum-quanto and optimum-nvidia to keep CLI startup fast, and imports each distribution’s subpackage module. That module, in turn, registers backend commands onto the CLI. In other words, Optimum’s plugin system is just Python packaging: there is no registry file to edit, no entry_points ceremony — install optimum-neuron and optimum-cli grows Inferentia capabilities.

This is also why the installation table in the README is the real user interface of the project. pip install "optimum[onnx]" gets you pure ONNX export; optimum[onnxruntime] adds the inference wrappers; optimum[openvino], optimum[neuronx], optimum[habana], optimum[amd] and optimum[furiosa] each pull a different hardware stack with its own GitHub repository and docs. The core repo deliberately holds none of that code. When you read a tutorial that imports from optimum.onnxruntime import ORTModelForSequenceClassification, that import resolves through the optimum namespace into the optimum-onnxruntime distribution — the class exists, but it does not live in this repository anymore. ExecuTorch support follows the same pattern through optimum-executorch, PyTorch’s native on-device runtime for mobile and edge (our ExecuTorch tour covers that runtime itself).

The payoff of the thin-core design is that shared behavior only gets written once. Config loading, weight saving, tokenizer persistence and task inference are core concerns; hardware-specific graph surgery is not.

From Install to Your First Model

Install the ONNX Runtime extra (the --upgrade-strategy eager flag matters because the subpackages pin tightly against each other):

python -m pip install --upgrade --upgrade-strategy eager optimum[onnxruntime]

The fastest path is the CLI, which exercises ExportCommand and the TasksManager for you:

optimum-cli export onnx --model gpt2 gpt2_onnx/

Programmatically, the canonical flow — straight from the repo’s own quickstart documentation — swaps one import. Setting export=True tells from_pretrained to trace the PyTorch checkpoint to ONNX during loading:

from optimum.onnxruntime import ORTModelForSequenceClassification
from transformers import AutoTokenizer

model_checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
save_directory = "tmp/onnx/"

# Load from the Hub and export to ONNX in one call
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
ort_model = ORTModelForSequenceClassification.from_pretrained(model_checkpoint, export=True)

# Persist the ONNX graph and the tokenizer together
ort_model.save_pretrained(save_directory)
tokenizer.save_pretrained(save_directory)

That save_pretrained call is inherited from OptimizedModel (modeling_base.py, L118) and reuses the shared save utilities that persist tokenizers next to optimized weights. Quantization is a second, symmetric step — build a quantization config, wrap the exported model in a quantizer, call quantize:

from optimum.onnxruntime import ORTQuantizer
from optimum.onnxruntime.configuration import AutoQuantizationConfig

qconfig = AutoQuantizationConfig.arm64(is_static=False, per_channel=False)
quantizer = ORTQuantizer.from_pretrained(ort_model)
quantizer.quantize(save_dir=save_directory, quantization_config=qconfig)

The result is a model_quantized.onnx file you can reload with the same ORTModelForSequenceClassification.from_pretrained(save_directory, file_name="model_quantized.onnx") call and run with a plain forward pass — no framework lock-in, since the weights are ONNX tensors on disk and the runtime is just ONNX Runtime.

The Routing Brain: TasksManager

Everything above depends on one deceptively hard question: given a model id, what is it for? TasksManager (L118) answers it with a stack of class-level mapping tables — _TRANSFORMERS_TASKS_TO_MODEL_LOADERS, _DIFFUSERS_TASKS_TO_MODEL_LOADERS, _TIMM_TASKS_TO_MODEL_LOADERS — that tie task tags like text-classification or automatic-speech-recognition to auto-model class names. The mappings tolerate ambiguity: where one task maps to several candidate loaders (for example speech models that can be AutoModelForSpeechSeq2Seq or AutoModelForCTC), the value is a tuple and the auto-class’s internal _model_mapping picks the right one.

Inference flows through three cooperating methods. infer_task_from_model (L800) reads the model config’s task hints, falling back to Hub metadata and eventually to head-inspection heuristics. infer_library_from_model (L958) decides whether the object belongs to transformers, diffusers, timm or sentence-transformers. Then get_model_from_task (L1083) instantiates the right auto-class, and determine_framework (L582) settles the PyTorch-versus-TensorFlow question with a version- and availability-aware policy. Extension is a first-class citizen: create_register (L290) returns a decorator that backend packages use to register their config classes per model type and task, and get_all_tasks (L1063) plus synonyms_for_task (L420) normalize the task vocabulary so text-classification and friends resolve consistently.

The same file also normalizes config chaos. Different architectures name the same concept differently — one model’s num_attention_heads is another’s d_model — and standardize_model_attributes (L1004) papers over those differences before export. The per-architecture config wrappers themselves live in NormalizedConfigManager (optimum/utils/normalized_config.py, L209), which exporters consult to find the right attribute names for inputs, outputs and dynamic axes.

Quantization: the Contract and the Flagship

The quantization surface starts from a deliberately tiny abstract base. OptimumQuantizer (quantization_base.py, L10) declares from_pretrained and an abstract quantize (L23) and nothing else. Every backend quantizer — ORTQuantizer for ONNX Runtime, OVQuantizer with NNCF for OpenVINO — shapes itself to that two-call contract, which keeps user code portable across hardware.

One quantizer is still implemented in the core repository: GPTQ. GPTQQuantizer (optimum/gptq/quantizer.py, L64) is a complete post-training quantization flow for PyTorch modules. Its quantize_model method (L334) runs the layer-wise GPTQ algorithm against a calibration set, select_quant_linear (L199) picks the best available quantized-linear kernel for your hardware, and after quantization post_init_model (L607) and pack_model (L627) wire in the packed kernels and optionally compile the result. Calibration corpora are built in gptq/data.py — prepare_dataset (L34) assembles wikitext2, C4 or PTB samples — and evaluate_perplexity (optimum/gptq/eval.py, L7) gives you a quick degradation check before you ship. The layer-discovery helper get_layers (optimum/gptq/utils.py, L33) finds the nn.Linear-family modules worth quantizing, including transformers’ quirky Conv1D layout.

torch.fx: Rewrites and Tensor Parallelism

The third pillar is a torch.fx toolkit that predates much of PyTorch’s own parallelism tooling. On the optimization side, Transformation (L85) defines a lint-and-recompile wrapper with signatures and per-node bookkeeping, and ReversibleTransformation (L176) adds undo. Concrete rewrites include MergeLinears (L237), which fuses adjacent linear layers sharing an input into one bigger GEMM, and FuseBiasInLinear (L393), which folds bias adds into the matmul. These are the graph-level tricks exporters and runtime benchmarks both benefit from.

On the parallelism side, parallelize_model (optimum/fx/parallelization/api.py, L46) takes a model plus a device mesh and produces a sharded module. A shared ParallelExecutionCtx (core.py, L107) carries world size and process groups through the pass framework (PassBase, passes.py, L45), where ParallelAxisSolverPass (L144) decides which tensor dimension each op should shard along. DecompositionInterpreter (decomp.py, L78) breaks composite ops into aten primitives so the per-op handlers in the Registry (op_handlers.py, L25) can map them onto sharded replacements like ColumnParallelLinear (linear.py, L33) and VocabParallelEmbedding (embedding.py, L25), with collectives such as all_reduce (dist_ops.py, L22) implemented as differentiable autograd functions.

Honest Limits

The subpackage split is the right call and it still hurts. Nine accelerator extras in the install table means nine dependency matrices to keep coherent, and the README’s insistence on --upgrade-strategy eager is a confession that version skew between optimum, optimum-onnx and optimum-onnxruntime is a recurring failure mode. Documentation has largely caught up, but search engines have not: many blog posts and Stack Overflow answers still import classes from optimum.onnxruntime paths that now resolve through a sibling distribution you may not have installed.

Deprecation churn is real, too. The codebase is mid-rename: ExportConfig is a deprecated alias that warns on subclassing and points to ExporterConfig (optimum/exporters/base.py, L68); from_pretrained warns that encoding the revision as model@revision will be removed in v1.23 (optimum/modeling_base.py, L331). Pin versions or read the changelog before upgrading.

Export fidelity has inherent ceilings. Tracing works on real tensors, so control flow that depends on tensor values gets frozen, and configs gate on MIN_TORCH_VERSION via is_torch_support_available (L211) because some operators only export cleanly on newer toolchains. Post-export validation compares outputs within an absolute tolerance and raises typed errors like AtolError when numerically sensitive models drift — a safety net, not a fix. And while GPTQ quantization in the core is solid, the wider quantization story is deliberately fragmented across backends, so a quantized model built for one runtime is not guaranteed to load in another.

Finally, maintainership is spread thin by design. Optimum competes for the same “serve this model fast” niche as serving-first stacks like TGI and vLLM, and the honest answer is that Optimum is the conversion-and-optimization layer you reach for when you own the deployment target yourself.

Where Optimum Fits

Optimum is best understood as the bridge between the HuggingFace modeling ecosystem and the hardware below it. Above, it accepts anything Transformers can load; below, it emits artifacts for runtimes from ONNX Runtime through OpenVINO to edge runtimes, and it quantizes along the way. If your deployment story starts with “a model checkpoint from the Hub” and ends with “a specific chip,” the path runs through this repo’s contracts — the task router, the exporter config, the optimized model base — even when the code doing the heavy lifting lives in a sibling optimum-* package.

Watch PyShine on YouTube

Contents