Repository navigation
LLM Inference Roofline Migration Plan
opal.llm_inference replaces the old opal.config.llm_model.OpalModelConfig
-
opal.infra.gpu_model.GPUModelpair with a single package that (a) loads and interprets a model'sconfig.jsonin a quantization/MoE/MLA-aware way, and (b) estimates per-batch execution time from those dimensions using a roofline model, instead of the old model's fixeda/bcoefficients alone.
The old GPUModel timing math used two hand-tuned coefficients (a for the
attention pairwise-interaction cost, b for a fixed-shape dense/FFN term)
calibrated against a small set of dense GQA models. It has no notion of
mixture-of-experts, multi-head latent attention (MLA), sparse attention, or
Mamba/SSM hybrids — architectures increasingly common in current model
releases (DeepSeek-V3, Kimi-K2.x, GLM-5.x, Nemotron-H/Ultra). Extending the
old model per-architecture would have meant more special-case branches inside
GPUModel itself. Instead, opal.llm_inference models FLOPs and bytes
directly from real config dimensions, with architecture-specific subclasses
only overriding the attention term.
The old a/b engine isn't removed — it's kept as legacy_gpu_model.GPUModel
and remains the default (worker.inference_params.model: "roofline") for
every config that predates this change, so existing simulation results are
unaffected until a config opts in.
opal/llm_inference/
opal_model.py OpalModel, MoEParams, parse_opal_model_fields
config_loader.py ModelConfigLoader, detect_architecture, estimate_params, dtype helpers
roofline_inference_model.py GPUConfig, LLMRooflineModel + 4 architecture subclasses
legacy_gpu_model.py GPUModel (old a/b roofline + synthetic engine), unchanged behavior
inference_model_factory.py ModelFactory: config.json -> concrete LLMRooflineModel subclass
inference_engine.py build_inference_engine(): picks legacy vs. new, per worker.inference_params.model
OpalModel (successor to OpalModelConfig) loads a model's config.json
(from model-configs/<name>/, or Hugging Face as a fallback — see
ModelConfigLoader) and exposes:
-
get_model_params()/active_params— total vs. active (MoE-aware) parameter count -
weight_bytes_per_elem— quantization-aware (NVFP4 → 0.5, FP8 → 1.0, bf16/fp16 → 2.0), not a hardcoded* 2 -
key_value_bytes,get_kvc_bytes(tokens)— per-token KV cache footprint, MLA-aware (a compressed latent, not full K/V) -
num_hidden_layers,kv_head_size,num_key_value_heads,max_position_embeddings, etc. — same accessorsOpalModelConfigprovided, sokvc_manager.pyneeded no logic changes to switch over.
config_loader.estimate_params() derives total/active param counts
layer-by-layer from config dimensions alone (no weight download), handling
dense, MoE (with shared experts), LatentMoE, and Mamba/hybrid per-layer
patterns — validated to within a few percent of published param counts for
DeepSeek-V3, Kimi-K2.x, GLM-5.x, and Nemotron-H/Ultra.
LLMRooflineModel (abstract base, itself an OpalModel) computes
estimate(batch) -> {"time_s": ..., "flops": ..., "bytes": ...} as
max(flops / (compute_efficiency * peak_flops), bytes / (memory_efficiency * peak_bandwidth)) — a standard compute-vs-memory roofline, unlike the legacy
engine's two independent prefill/decode maxes (see legacy_gpu_model.GPUModel.estimate()
for that distinction spelled out).
Dense/FFN FLOPs and MoE weight-read bytes are shared in the base class; subclasses override only the attention term:
| Class | Attention pattern | KV cache footprint |
|---|---|---|
FullAttentionRooflineModel |
dense causal, quadratic in prefill |
2 * num_kv_heads * head_dim per token per layer (GQA/MHA) |
MLAAttentionRooflineModel |
same access pattern as full attention | small latent dim (kv_lora_rank + RoPE dim) — ~40x smaller than the GQA formula for GLM-5.2-NVFP4 |
SparseAttentionRooflineModel |
top-k (DSA-style): linear in context, not quadratic | only the topk selected entries read back per decode step |
HybridMambaMoERooflineModel |
few dense attention layers + Mamba-2 selective scan (flops_extra/bytes_extra) |
recurrent state carries forward; only new tokens pay the scan cost |
ModelFactory (inference_model_factory.py) picks the right subclass via
config_loader.detect_architecture(), which checks hallmark config fields
(kv_lora_rank → MLA, index_topk → sparse attention, mamba_d_state /
layers_block_type → hybrid Mamba) rather than a model-name allowlist.
build_inference_engine(opal_env, opal_config) reads
worker.inference_params.model: "llm_inference" resolves to the new
ModelFactory-built engine; anything else ("roofline", "synthetic", the
default) resolves to the legacy GPUModel, preserving existing behavior
exactly. OpalSimulatorEnvironment.initialize() builds this once per
simulation and stores it as self.inference_engine; every worker reads the
same shared instance in its own __init__ (self.gpu_model = self.opalEnv.inference_engine) instead of each constructing its own
GPUModel — rebuilding an identical engine per worker was pure redundant
work once every worker already shares the same opal_config.
vllm_worker.LLMWorkerVLLMScheduler._calculate_batch_time then calls
self.gpu_model.estimate(batch)["time_s"] uniformly — see vLLM Worker's
Timing Calculation section.
Ported (semantically) from internal commit c52fbeb, merged to main as
5fca50c (fixes #45, #46):
-
669c3ce— add the package, unwired. -
f65ab9c— cut overenvironment.py/kvc_manager.py/vllm_worker.py. - Delete
opal/config/llm_model.py; replaceopal/infra/gpu_model.pywith a re-export shim toopal.llm_inference.legacy_gpu_model.GPUModel.
All three steps are complete.
Not carried over: internal repo's vllm_worker.py also references an
opal.apc_policy module (GPU prefix-caching admission policy) in the same
constructor region touched by this migration. That module doesn't exist in
this repo and wasn't part of c52fbeb's diff — it belongs to separate,
not-yet-ported GPU HBM Automatic Prefix Caching work.