Runtime core¶
This guide covers the layer under ExplainRunner: the run
archive, coalition deduplication and the async scheduler. Most users only meet it
through explanation.archive, dedup and max_inflight; the snippets show
the pieces directly. They share one namespace.
Run archives¶
An archived run is a directory with manifest.json, the base generation and
one row per coalition output in archive.sqlite. Run an explanation with an
archive path and read the rows back with
load():
from pathlib import Path
from mllm_shap import (
ConversationSnapshot,
ExplainConfig,
ExplainRunner,
Message,
Role,
Turn,
)
from mllm_shap.runtime import RunArchive
snapshot = ConversationSnapshot.from_turns((
Turn(messages=(Message(role=Role.USER, text="Who are you?"),)),
))
config = ExplainConfig.model_validate({
"backend": {"kind": "mock", "model_id": "mock"},
"explanation": {
"estimator": "exact",
"absence_policy": "delete",
"archive": {"path": "runs/{run_id}"},
},
})
output = ExplainRunner(config).explain_sync(snapshot)
root = Path("runs") / output.run_id
print("files:", sorted(path.name for path in root.iterdir()))
with RunArchive.load(root) as archive:
print("base:", repr(archive.read_base_generation()[:19]))
for record in list(archive.history_lazy())[:3]:
print(record.record_id, repr(record.generation_text[:19]))
Output
files: ['archive.sqlite', 'base_generation.txt', 'manifest.json']
base: 'bdb7 1b7c 339c e75d'
1 '305a 3f1d f172 895d'
2 '4280 896d bf1c 7f28'
3 '9663 286e 6d79 039a'
Re-running the same configuration on the same snapshot targets the same
directory. open() creates archives and
refuses to append to an existing one, so the second run raises
ArchiveExistsError; on_exists: overwrite deletes the
old rows, manifest and base generation first:
from mllm_shap.errors import ArchiveExistsError
try:
ExplainRunner(config).explain_sync(snapshot)
except ArchiveExistsError as error:
print(type(error).__name__)
overwrite = config.model_copy(
update={
"explanation": config.explanation.model_copy(
update={"archive": config.explanation.archive.model_copy(
update={"on_exists": "overwrite"},
)},
),
},
)
again = ExplainRunner(overwrite).explain_sync(snapshot)
print("same run id:", again.run_id == output.run_id)
Output
ArchiveExistsError
same run id: True
Run manifest¶
Every archive stores manifest.json and one SQLite file, archive.sqlite
(WAL journal), whose rows hold the generated text. The manifest’s
schema_version is MANIFEST_SCHEMA_VERSION
and the database’s PRAGMA user_version is
ARCHIVE_SCHEMA_VERSION.
load() refuses archives with any other
version, such as those written by 0.1.17 or earlier, with
ManifestVersionError; re-run explain to rebuild them.
The manifest written by ExplainRunner records:
Field |
Meaning |
|---|---|
|
Manifest format and the package version that wrote it |
|
Run identifier and the explained conversation snapshot |
|
How payoffs were sampled, scored and aggregated |
|
How the prompt was partitioned and masked; |
|
Which backend and model generated the coalition outputs |
|
Full |
reanalyze() rescores an archive with the runner’s
value function and normalizer, re-aggregates with the archived estimator, and
raises ReanalyzeMismatchError when the runner’s
estimator, absence policy or partition differs from the archived run. With the
same value function it reproduces the explain attributions; How a run works
has an example.
When every coalition was generated the pipeline marks the archive complete.
reanalyze() raises
ArchiveIncompleteError for an archive of a run that failed
part-way, one without the grand or empty coalition, a full-budget archive missing
rows, or an archive whose manifest was not written by
ExplainRunner.
Build coalition dedup keys¶
A coalition key hashes everything that determines a generation: snapshot, player
order, mask, absence policy, model and generation settings. With dedup.enabled:
auto deduplication is on only for deterministic decoding (temperature 0):
from mllm_shap.pipeline.config import DedupConfig, GenerationConfig
from mllm_shap.runtime import build_coalition_key, dedup_enabled
generation = GenerationConfig(temperature=0.0)
key = build_coalition_key(
snapshot_id="snap-1",
player_ids=("snap-1:0:0:0", "snap-1:0:0:1"),
mask_present=(True, False),
absence_policy="delete",
model_id="mock",
generation=generation,
)
print(dedup_enabled(DedupConfig(enabled="auto"), generation), key[:12])
Output
True ec7d60731ab2
Schedule async coalition jobs¶
InferenceScheduler checks the hot store, then the dedup
registry, and only then calls generate under a max_inflight semaphore.
Nine jobs over three distinct keys cost three calls:
import asyncio
from mllm_shap.masking.codec import MaskCodec
from mllm_shap.runtime import (
CoalitionDedupRegistry,
CoalitionJob,
HotResultStore,
InferenceScheduler,
)
packed = MaskCodec.pack((True, False, True))
async def generate(prompt: ConversationSnapshot) -> str:
return prompt.turns[0].messages[0].text
scheduler = InferenceScheduler(
max_inflight=2,
generation=GenerationConfig(temperature=0.0),
store=HotResultStore(),
dedup=CoalitionDedupRegistry(),
)
jobs = [
CoalitionJob(
coalition_key=f"key-{index % 3}",
snapshot_id=snapshot.snapshot_id,
snapshot=snapshot,
absence_policy="delete",
mask_words=packed.words,
mask_n_bits=packed.n_bits,
model_id="mock",
)
for index in range(9)
]
metrics = asyncio.run(scheduler.run(jobs, generate))
print(metrics)
Output
SchedulerMetrics(requested=9, executed=3, deduplicated=0, cache_hits=6, kv_cache_hits=0, kv_cache_reused_tokens=0)
Pass progress= (any object with on_coalitions_planned and
on_coalition_finished) to observe the counts; How a run works shows one. The
counts are cumulative over every run of one scheduler: when Neyman’s second phase
adds jobs, on_coalitions_planned is called again with the larger total and the
finished count keeps growing.
Streaming large job sets¶
run_iter() accepts any
iterable and only creates coroutines as slots free up, so an exact run over many
players never holds all \(2^n\) jobs in memory at once:
from mllm_shap import ExactEstimator
from mllm_shap.masking import MaskBuilder, PadPolicy, TokenPartitioner
players = TokenPartitioner().partition(snapshot)
builder = MaskBuilder(PadPolicy())
def job_stream():
for mask in ExactEstimator.iter_masks(players):
packed = MaskCodec.pack(mask.present)
yield CoalitionJob(
coalition_key=packed.words.hex(),
snapshot_id=snapshot.snapshot_id,
snapshot=builder.render(builder.view(snapshot, players, mask)),
absence_policy="pad",
mask_words=packed.words,
mask_n_bits=packed.n_bits,
model_id="mock",
)
streaming = InferenceScheduler(
max_inflight=4,
generation=GenerationConfig(temperature=0.0),
store=HotResultStore(),
)
metrics = asyncio.run(streaming.run_iter(job_stream(), generate, total=8))
print(metrics.requested, metrics.executed)
Output
8 8
Failure semantics¶
Jobs run in one asyncio.TaskGroup. When any generate call raises,
the task group cancels every other job and the scheduler re-raises that first
exception itself, not wrapped in an ExceptionGroup, so except
RuntimeError works as written; a run never returns partial metrics or a
partially aggregated result:
async def flaky(prompt: ConversationSnapshot) -> str:
if "[MASK]" in prompt.turns[0].messages[0].text:
msg = "backend refused a masked prompt"
raise RuntimeError(msg)
return "ok"
failing = InferenceScheduler(
max_inflight=2,
generation=GenerationConfig(temperature=0.0),
store=HotResultStore(),
)
try:
asyncio.run(failing.run_iter(job_stream(), flaky, total=8))
except RuntimeError as error:
print("raised:", error)
Output
raised: backend refused a masked prompt
Logging¶
Importing mllm_shap leaves the host application’s logging alone. Opt in with
configure_logging():
import mllm_shap
mllm_shap.configure_logging(level="INFO") # plain lines on stderr
mllm_shap.configure_logging(level="INFO", json=True) # one JSON object per line
mllm_shap.configure_logging("config/logging.example.toml") # a [tool.logging] file
Without arguments it applies the [tool.logging] section of the nearest
pyproject.toml. Setting MLLM_SHAP_LOGGING_CONFIG to a TOML file applies that
file on import instead; config/logging.example.toml in the repository is a
starting point. At INFO the logger mllm_shap.pipeline.orchestrator reports
each stage once:
Event |
Message |
|---|---|
Run start |
|
Base generation |
|
Coalitions scheduled |
|
Run end |
|
DEBUG adds HTTP retries and skipped plugin entry points; LM Studio model
downloads are logged at INFO by mllm_shap.backends.lmstudio.models.
Telemetry and progress¶
InMemoryObservabilitySink records one
SpanRecord (name and duration) per pipeline
stage, and any CoalitionProgress receives the
coalition counts:
from mllm_shap import InMemoryObservabilitySink
class CountingProgress:
def __init__(self) -> None:
self.planned: list[int] = []
self.finished = 0
def on_coalitions_planned(self, total: int) -> None:
self.planned.append(total)
def on_coalition_finished(self, done: int, total: int) -> None:
self.finished = done
sink = InMemoryObservabilitySink()
progress = CountingProgress()
ExplainRunner(overwrite, telemetry=sink, progress=progress).explain_sync(snapshot)
print("stages:", [span.name for span in sink.spans])
print("planned:", progress.planned, "finished:", progress.finished)
Output
stages: ['orchestrator.generate_base', 'estimator.sample_masks', 'orchestrator.schedule', 'orchestrator.score', 'orchestrator.aggregate', 'orchestrator.run']
planned: [8] finished: 8
A progress bar needs nothing more. With tqdm installed:
from tqdm import tqdm
class TqdmProgress:
def __init__(self) -> None:
self.bar = tqdm(total=0, unit="coalition")
def on_coalitions_planned(self, total: int) -> None:
self.bar.total = total
self.bar.refresh()
def on_coalition_finished(self, done: int, total: int) -> None:
self.bar.update(done - self.bar.n)
ExplainRunner(config, progress=TqdmProgress()).explain_sync(snapshot)
The callbacks run synchronously on the event loop, so keep them cheap.
Performance benchmarks¶
Benchmarks live in tests/benchmarks/ and use pytest-benchmark. They cover
coalition deduplication keys, mask pack/unpack, 10k-row archive round trips,
prefix-cache lookups, exact, stratified and complementary aggregation, the
scheduler with 1 000 jobs, TF-IDF scoring and an end-to-end mock run.
make bench # run them and write .pytest_benchmark/benchmark.json
make bench-regression # fail when a median is >25% slower than baseline.json
tests/benchmarks/baseline.json was recorded on 2026-10-12 on an Apple M4
(10 cores, 16 GB RAM), macOS 26.5.1 (Darwin 25.5.0), CPython 3.12.12. Compare against
it only on comparable hardware; on another machine record a local baseline first.
make check does not run benchmarks; a weekly CI workflow runs make bench
and keeps the report as an artifact. Run make bench-regression before changing
hot paths in runtime/, masking/ or estimation/.
Notebook¶
For a runnable walkthrough with stored outputs, see
examples/runtime_core.ipynb (also embedded on Examples).