BLIS Extension Recipes¶
Step-by-step guides for extending BLIS. Each recipe lists the exact files to touch, the order, and examples to follow.
Routing, Admission and Flow Control¶
BLIS has no routing, admission or flow-control code of its own: every cluster run uses the llm-d router embedded in-process (Routing). New routing, admission or flow-control behavior belongs in llm-d-router, as a plugin or configuration, not in this repository.
A new llm-d plugin type becomes usable in BLIS once it is audited and added to the supported list in sim/llmd/support.go: read every clock read, goroutine, blocking wait, random draw and map iteration it makes, route the decision-relevant ones through the hosting runtime in the fork, check that every signal it consumes is produced by the simulated engines, and add a test in sim/llmd. Until then it is refused at startup with the reason in the unsupported list. See The llm-d router fork.
Adding New Instance Schedulers¶
To add a new instance-level scheduler (wait-queue ordering):
- Implement the
InstanceSchedulerinterface insim/scheduler.go(receivesrequests+clockonly). - Register in two places (both required): add the name to
validSchedulersinsim/bundle.go, and add acasetoNewSchedulerinsim/scheduler.go. CLI error messages derive fromValidSchedulerNames(). - Add tests following BDD naming:
TestMyScheduler_Scenario_Behavior. Test observable behavior, not internal structure. - Update documentation: the relevant
docs/guide. TouchCLAUDE.mdonly if you are adding an operating rule an agent needs every session (see the charter at the top of CLAUDE.md) — it holds pointers, not per-feature detail.
Examples:
- See PriorityFCFSScheduler and SJFScheduler in sim/scheduler.go.
Extending KV Cache Tiers¶
To add a new KV tier (e.g., NVMe offloading for 3-tier GPU+CPU+NVMe):
- Implement the
KVStoreinterface insim/kv/(11 methods: allocate, get cached, release, capacity queries, metrics,SetClock,ConsumePendingTransferLatency) - Compose existing tiers — e.g., wrap
TieredKVCache(GPU+CPU) with NVMe logic, following the same delegation pattern - Update
NewKVStorefactory insim/kv/register.goto instantiate your tier based onKVCacheConfigfields (add new fields toKVCacheConfiginsim/config.go) - Add CLI flags in
cmd/root.gofor new parameters (e.g.,--kv-nvme-blocks) and wire them into theKVCacheConfigsub-config Tier transfer time is not computed here: each tier names a catalogdevice_class, andblis-latency-kernelprices the transfer (TierTime). A new device class is added toblis-catalog. - Aggregate metrics — combine hit/miss/thrashing counters from all tiers; see
TieredKVCache.CacheHitRate()for the 2-tier pattern - Add behavioral tests in
sim/kv/*_test.go - Check-then-act allocation (no rollback) —
KVCacheState.AllocateKVBlocksuses a pre-check gate: it computes the total blocks needed (new blocks + cached blocks leaving the free list) and compares againstcountFreeBlocks()before any state mutation. If insufficient, it returnsfalseimmediately with zero side effects. Post-pre-checkpopFreeBlock() == nilis apanic(INV-4 violation, structurally unreachable in single-threaded DES). This mirrors vLLM'skv_cache_manager.py:334-336universal pre-check. If your tier adds mutations before delegating togpu.AllocateKVBlocks(), ensure the inner pre-check sees the updatedFreeBlockCnt(e.g.,commitCachedBlockscallsremoveFromFreeListwhich decrementsFreeBlockCntbefore the inner call). GetCachedBlocksis a pure query — it returns cached block IDs without side effects.CacheHitsare counted byAllocateKVBlockswhen cached blocks are committed to an allocation. The pre-check accounts for cached blocks with!InUse(on the free list) via thecachedFromFreeListbudget, mirroring vLLM'snum_evictable_blocks.
Examples:
- See TieredKVCache in sim/kv/tiered.go for 2-tier GPU+CPU composition
- See KVCacheState in sim/kv/cache.go for single-tier baseline (also implements KVStore)
- See docs/plans/archive/pr12-architectural-predesign.md for the design decisions behind the tiered architecture
Adding New Trace Record Types¶
To add a new trace record type (e.g., ScaleRecord for autoscaling events):
- Define the record struct in
sim/trace/record.go(pure data, nosim/dependency) - Add a slice field to
SimulationTraceinsim/trace/trace.go(e.g.,Scales []ScaleRecord) - Add a recording method to
SimulationTrace(e.g.,RecordScale(ScaleRecord)) - Hook recording into the cluster pipeline (e.g.
sim/cluster/gateway.gofor router decisions,sim/cluster/cluster_event.gofor scaling events; guard withif cs.trace != nilfor zero-overhead default) - Update
Summarize()insim/trace/summary.goto aggregate the new record type - Add behavioral tests in
sim/trace/*_test.go
Examples:
- See AdmissionRecord in sim/trace/record.go for a simple record
- See RoutingRecord with CandidateScore for a record with nested counterfactual data
- See counterfactual() in sim/cluster/gateway.go for derived computation that lives in sim/cluster/ (not sim/trace/): it ranks the scores the router's primary scheduling profile gave every endpoint
Adding Fields to TraceV2 Format¶
To add a new field to TraceV2 CSV output (e.g., observability metadata like vllm_priority):
- Add field to
TraceRecordinsim/workload/tracev2.go(place logically near related fields) - Add field to
RequestRecordincmd/observe.go(if captured during observation) - Capture the value in
RealClient.Send()incmd/observe.go(set field onRequestRecord) - Wire through Recorder in
cmd/observe.go: UpdateRecordRequest()to copy field fromRequestRecordtoTraceRecord - Update ExportTraceV2 in
sim/workload/tracev2.go: - For optional columns: scan records to determine if column is needed, conditionally add to header, conditionally write values
- For always-present columns: add to
traceV2Columnsslice and write in row construction - Update LoadTraceV2/parseTraceRecord in
sim/workload/tracev2.go: - For optional columns: detect column presence from CSV header, apply offset to subsequent column indices
- For always-present columns: parse at fixed index, update all downstream indices
- Enforce simulation isolation (if observability-only): Verify
LoadTraceV2RequestsandLoadTraceV2SessionBlueprintsdo NOT read the field intosim.Request— add tests for this - Add behavioral tests covering round-trip, conditional logic, and simulation isolation
Column position guidelines:
- Observability metadata: place near related fields (e.g., vllm_priority after slo_class)
- Timing data: group with other timestamps
- Optional columns: prefer conditional inclusion over always-writing zero values
Examples:
- See PR #1220 (VLLMPriority) — optional column, simulation-isolated, conditional on SLOClass presence
- See FinishReason/ErrorMessage fields — always-present optional strings (empty when not set)
- See ServerInputTokens field — observability metadata that differs from InputTokens
Extending Latency Pricing¶
Latency pricing is not extended in this repository. BLIS takes every step time, memory figure, P/D transfer time, offload tier transfer time and host overhead from blis-latency-kernel (adapter sim/kernelmodel); there is no LatencyModel factory or backend flag to add a case to.
- A new pricing term, engine feature or model shape → change
blis-latency-kernel, release it, and bump the pinned module version ingo.mod. - New or refitted coefficients → publish them in
blis-registryand name the coefficient sets in the scenario'scoefficients:list. - A new model, chip, fabric or storage device → add it to
blis-catalog. - A new scenario or engine-setting field → change the format in
blis-schemasfirst.
On the BLIS side, a new kernel capability usually needs only the adapter in sim/kernelmodel and the scenario-to-SimConfig wiring in cmd/. See Latency models.
Adding New Batch Formation Strategies¶
To add a new batch formation strategy (e.g., disaggregated prefill/decode, speculative decoding, continuous batching without preemption):
- Implement the
BatchFormationinterface insim/batch_formation.go(or a new file for complex strategies) — 1 method: FormBatch(ctx BatchContext) BatchResult— compose the running batch for the next step- The implementation receives
BatchContextwith: RunningBatch, WaitQ, KVCache, token budget, batch size limit, chunked prefill threshold, MaxModelLen (0 = unlimited; implementations should clamp token scheduling tomaxModelLen-1-ProgressIndexwhen > 0), simulation time, step count, and ComputedTokens map - The implementation MUST update
ctx.ComputedTokens[req.ID]for each request that receives new tokens (Phase 2 ofStep()reads this map to advanceProgressIndex) - The implementation may mutate
WaitQ(dequeue/prepend) andKVCache(allocate/release) during batch formation - The implementation MUST NOT schedule events or record metrics — return decisions in
BatchResult, the Simulator applies them - Register in
NewBatchFormationfactory insim/batch_formation.go: add a selection branch. The factory signature isNewBatchFormation(preemptionPolicy string). For a new batch formation strategy (not just a preemption variant), add aBatchFormation stringfield toPolicyConfigand a selection branch inNewBatchFormation - Add behavioral tests — token budget enforcement, batch size limits, KV conservation, preemption behavior (if applicable), FCFS ordering
- Extension friction: 2 touch points (implementation + factory registration)
Note: Currently only VLLMBatchFormation exists (with configurable preemption via --preemption-policy fcfs|priority). Adding a second batch formation strategy will also require: (a) a BatchFormation string field in PolicyConfig or BatchConfig (in sim/config.go), (b) a CLI flag in cmd/root.go, (c) validation in sim/bundle.go, (d) selection logic in NewBatchFormation. For adding a new preemption variant (not a new strategy), add a constant to batch_formation.go, a case to the switch in preemptForTokens, and an entry in validPreemptionPolicies in bundle.go.
Examples:
- See VLLMBatchFormation in sim/batch_formation.go for the vLLM FCFS + chunked-prefill + preemption strategy
- See preemptForTokens for the KV allocation + eviction loop pattern
Adding New Quantization Formats¶
Quantization is stated per pool in the scenario (engine.quantization, engine.cache_dtype) and priced by blis-latency-kernel. A new format is added there (and to blis-schemas if the field's allowed values change), not in this repository.
Adding a New Engine¶
To add a new autoscaler Engine implementation (e.g., a cost-minimizing MIP solver or an OpenEvolve-evolved policy):
- Implement the
Engineinterface insim/cluster/engine.go(or a new file for complex engines) — 1 method: Optimize(results []AnalyzerResult, inventory GPUInventory) []ScaleDecision- Inputs: one
AnalyzerResultper model (supply/demand signals + per-variant breakdown) and aGPUInventorysnapshot (free GPU slots per variant, pre-subtracted for Loading/Active/Draining instances). - Output: at most one
ScaleDecisionper model per call.Delta > 0= add replicas;Delta < 0= remove replicas. -
Must not read
RouterStateorModelSignalsdirectly — onlyAnalyzerResult. -
Reuse the shared helpers when appropriate:
scaleUpN(requiredCapacity float64, vcs []VariantCapacity) int— exact replica count viaceil(requiredCapacity / prc); fallback to 1 whenprc == 0.scaleDownN(spareCapacity float64, vc VariantCapacity) int— exact replica count viafloor(spareCapacity / prc), clamped to[1, ReplicaCount]; fallback to 1.sortedByAscCost,sortedByDescCost— deterministic variant sort (R2: copy-and-sort to avoid mutating caller data).-
Pass only the selected variant to
scaleUpN(vcs[0:1], not the full slice) soperReplicaCapacityForScaleUpuses the chosen variant's own capacity — not a different active variant's. -
Wire the engine in
sim/cluster/cluster.go— search for&UnlimitedEngine{}in the autoscaler pipeline construction and replace it with your engine (or add a config field + factory once multi-engine selection is implemented in the follow-up wiring PR). -
Add behavioral tests in
sim/cluster/engine_test.go: - Scale-up: correct
Delta, correctVariant, inventory check pass/fail. - Scale-down: correct
Deltafrom spare capacity,ReplicaCountclamp. - Edge cases: zero
RequiredCapacity, zeroSpareCapacity, all variants inactive,TPDegree > 1. -
Cross-model: multiple
AnalyzerResultentries — verify decisions for each model independently. -
Extension friction: 2 touch points (implementation + wiring via
&UnlimitedEngine{}incluster.go). For research variants behind a config field, 4 touch points: implementation +AutoscalerConfigfield + factory function + CLI flag.
Examples:
- See UnlimitedEngine in sim/cluster/engine.go for a simple inventory-ignoring engine
- See GreedyEngine in sim/cluster/engine.go for an inventory-respecting greedy engine with exact-N sizing
Adding New Per-Request Metric Fields¶
To add a new field to per-request JSON output (appears in --metrics-path output):
- Add field to
Requestinsim/request.go(runtime state, zero-value safe). When constructingRequeststructs, useRequestStatetyped constants (StateQueued,StateRunning,StateCompleted) — never bare strings. - Add field to
RequestMetricsinsim/metrics_utils.go(JSON output struct, useomitemptyfor backward compatibility) - Update
NewRequestMetrics()constructor insim/metrics_utils.goto propagate the new field fromRequesttoRequestMetrics - Set the field at the appropriate point (e.g., dispatch in
sim/cluster/gateway.gofor cluster-level, or completion for computed metrics) - Add behavioral tests covering multi-instance, single-instance, and standalone boundaries
Examples:
- See HandledBy (#181) — derived from the instance a request was dispatched to, zero-value outside the cluster pipeline (suppressed from JSON via omitempty)
- See SLOClass/TenantID (PR10) — set during workload generation, propagated at injection