Skip to content

Release v0.3.0: defensible calibration, fair benchmarks, and staged inference - #5

Merged
sohams25 merged 16 commits into
mainfrom
claude/earlyon-release-readiness
Jul 22, 2026
Merged

Release v0.3.0: defensible calibration, fair benchmarks, and staged inference#5
sohams25 merged 16 commits into
mainfrom
claude/earlyon-release-readiness

Conversation

@sohams25

@sohams25 sohams25 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Release v0.3.0: defensible calibration, fair benchmarks, and staged inference

Base: main. Head: the v0.3 release-readiness branch
(includes the hardening work; branch deleted after merge).

What this PR is

A correctness and methodology release. No new backbones, no new headline
claims — the calibration math, the measurement methodology, the
serialization contract and the public claims are made defensible, and every
fix is pinned by tests (261 → 265+ tests, 95% coverage, strict mypy).

The four core defects fixed

  1. One temperature for every head. Temperature scaling was fit once from
    the final classifier's logits and reused at every exit, though each exit
    head is a differently miscalibrated classifier. Now: per-head fit with
    convergence status, per-head application at routing time. A test proves a
    single global temperature misroutes where per-head does not.
  2. "Disabled" exits could fire. Disabling used threshold sentinels
    (confidence 1.0 / entropy 0.0) while routing compares with >=/<=, so
    a float32-saturated softmax fired a "disabled" exit. Now: explicit
    enabled_exits booleans; sentinel-carrying v1 checkpoints migrate.
  3. Invalid temperatures were clamped sharp. A negative fitted/user
    temperature was clamped to 1e-6, producing an artificially razor-sharp
    softmax. Now: centralized EarlyExitConfig.validate() raises; runtime
    guard falls back to the no-op 1.0.
  4. Benchmarks weren't comparable. The wrapper and backbone were measured
    on different inputs and boundaries. Now: benchmark_models feeds every
    compared model the identical preloaded sample sequence with identical
    warmup/boundary/sync; old records quarantined as legacy_v0_2.

Also in this PR

  • Staged calibration pipeline + rich CalibrationResult (enablement, exit
    distribution, sample count, honest greedy-coordinate-grid method name).
  • Checkpoint format_version: 2 + deterministic v1 migration, tested
    against a genuine v0.2-written fixture; factory= loading for custom
    models.
  • estimated_backbone_flops_fraction (alias kept): lazy FlopsEstimate
    with provenance, reused-module detection, exit-head cost disclosed.
  • Restartable tegrastats monitor; instantaneous power vs window-average vs
    integrated energy kept distinct; missing telemetry is None.
  • custom_ee: example args/kwargs, device inference, feature extractors,
    exactly-once/in-order exit validation.
  • Staged deployment contract + reference splitter for Sequential backbones,
    proven equivalent to eager routing and proven to skip later stages.
  • Packaging: wheel/sdist build + twine-clean; clean-env wheel smoke script;
    sdist no longer ships a partial test tree.
  • Bounded, seeded CUDA evidence run on CIFAR-10 (ResNet-18 EE vs backbone vs
    static MobileNetV2) with disjoint train/temperature/calibration/test
    splits — results in docs/evidence/.
  • Docs: calibration/benchmark contract, migration guide, staged deployment
    guide, security review, honest README repositioning
    ("deployment-oriented", legacy labels, batch-1 scope).

Compatibility

Old checkpoints load (migrated, warned). computation_used,
fitted_temperature and the single-model benchmark helpers remain as
compatible aliases/wrappers. Breaking edges are listed in
docs/MIGRATION.md.

Test plan

  • pytest full suite (CPU + CUDA host): all green
  • mypy earlyon strict, ruff, black --check, isort --check-only
  • wheel + sdist build, twine check, clean-venv install + out-of-repo
    smoke (scripts/smoke_test.py)
  • genuine v1 checkpoint migration fixture
  • bounded CUDA evidence run (scripts/evidence_run.py)
  • optional before tagging: full-length scripts/run_benchmarks.py on
    target hardware; Jetson procedure from docs/STAGED_DEPLOYMENT.md

sohams25 added 16 commits July 22, 2026 02:45
…tion, checkpoint v2

- EarlyExitConfig: per-head temperatures mapping (exits + final), explicit
  enabled_exits booleans, centralized validate() re-checked at wrapper
  construction and checkpoint load; legacy scalar temperature broadcast
  deterministically
- routing: disabled exits never fire (even at confidence 1.0 / entropy 0.0);
  each head's softmax uses its own temperature; invalid runtime temperatures
  fall back to 1.0 instead of a razor-sharp 1e-6 clamp
- calibration: staged pipeline (collect logits once, fit per-head
  temperatures with convergence status, greedy grid search on cache,
  evaluate); exits that never fire stay disabled; empty loaders fail fast;
  CalibrationResult carries exit distribution, sample count, objective,
  method and schema version
- checkpoints: format_version 2 with exit-point metadata and cross-check;
  deterministic v1 migration (scalar temperature broadcast, sentinel
  thresholds -> enabled_exits=False) with warnings; custom_ee loads via a
  user-supplied factory
- InferenceResult/BatchedInferenceResult: computation_used renamed to
  estimated_backbone_flops_fraction with a read alias
- benchmark_models: one measurement core; every compared model (backbone,
  early-exit wrapper, static baseline, quantised variant) sees the exact
  same preloaded sample sequence, warmup, eval/inference_mode setup, device
  placement and per-iteration synchronization
- explicit boundary labels (model-only vs end-to-end incl. H2D copy);
  loader-vs-noise input source labelled; accuracy reported alongside speed
  for labelled loaders; batch-1 validated as the primary mode
- BenchmarkResult carries dtype, input shape, warmup, sync policy, and
  avg_estimated_flops_fraction (computation_used kept as read alias)
- CLI benchmark + scripts use the fair runner; docs/benchmarks.json
  restructured with pre-v0.3 records quarantined under legacy_v0_2 and
  marked not comparable
- estimate_layer_flops returns FlopsEstimate (fractions + method + reliable
  + notes + excludes_exit_heads); per_layer_flops kept as thin wrapper
- reused/multi-call leaf modules are detected via a counting probe and
  produce a warned, low-confidence uniform fallback instead of a precise but
  wrong cumulative fraction
- EarlyExitWrapper computes the estimate lazily on first inference (cached),
  so constructing large models (ViT) is fast; probe runs on the backbone's
  current device and is materialised before routing flags flip
- estimator explicitly excludes exit-head FLOPs and routing overhead;
  documented on FlopsEstimate and InferenceResult
- TegrastatsMonitor.start() clears the stale stop event and sample buffer
  (a restarted monitor previously collected nothing), no-ops on double
  start, degrades to unavailable on spawn failure, and stop() closes the
  child's stdout pipe
- missing telemetry fields parse to None instead of invented zeros;
  JetsonRun documents its power as instantaneous
- integrate_energy(): pure trapezoidal integration over the timed window;
  EnergySummary distinguishes instantaneous power, window-average power and
  integrated energy, and reports energy-per-inference only when a real
  integral and inference count exist
- profiler exposes profile_with_energy; CLI profile reports the energy
  block and nulls for missing data; parser covers Orin-style rails
- example_args/example_kwargs for the inspection forward (single-tensor
  path retained); dry-run tensors created on the backbone's own device (or
  an explicit device) instead of blindly probing a CUDA model with CPU input
- per-layer feature_extractors convert tuple/dict/odd-rank outputs into the
  Tensor the exit head consumes, applied in the dry run AND in the routing
  hook (wrapper feature_adapters)
- the dry run now validates each exit layer executes exactly once and in
  the listed forward order; reused, missing or out-of-order exits fail with
  actionable errors instead of building a mis-routing wrapper
- freshly built exit heads placed on the backbone's device; wrapper FLOPs
  estimate degrades to a warned low-confidence uniform fallback when the
  probe cannot run the backbone
- earlyon.staged: StageSpec/Stage/StagedModel protocol — ordered stages
  emitting (continuation_features, exit_logits), routing applied between
  stages so later stages genuinely never run
- staged_model() splits Sequential backbones (custom_ee shape), refuses
  anything it cannot prove, and self-checks staged==eager on a probe input;
  tests pin equivalence across policies, per-head temperatures, enablement
- docs/STAGED_DEPLOYMENT.md: protocol, scope, per-stage ONNX/TensorRT
  procedure for later Jetson validation (no fabricated numbers)
- export_all_exits_to_onnx alias makes the static multi-output export's
  compute-everything semantics explicit in the API name
- README: deployment-oriented framing, batch-1 primary use case, legacy
  labels on pre-v0.3 numbers, routing-overhead and estimator limitations,
  static-baseline guidance, per-head temperature + enablement description
- DESIGN_DECISIONS: per-head temperature section now matches reality; new
  sections on explicit enablement and estimated-FLOPs naming
- docs/CALIBRATION_AND_BENCHMARK_CONTRACT.md: split discipline, pipeline
  stages, estimator limits, benchmark fairness rules
- examples: test-label hygiene note in 01; 02 rewritten on the fair runner
  and energy-aware profiler; pyproject description de-hyped
- CHANGELOG unreleased, RELEASE_NOTES_DRAFT.md (0.3.0, not tagged),
  OVERNIGHT_REPORT.md
ruff --fix (unused imports), noqa for sys.path-dependent imports, rename
ambiguous lambda vars, black/isort over scripts/ and examples/ which CI's
earlyon+tests scope never checked
…ng claims

- decisive counterfactual: a single global temperature misroutes where
  per-head temperatures do not (junk overconfident head vs final classifier)
- batched routing pinned to per-head temperatures (previously untested path)
- calibration determinism, NaN-emitting head safely disabled, clean NaN
  rejection in temperature fit, single-class calibration data
- staged runtime proven to skip later stages structurally (counting module)
- custom_ee dict-output feature extractor
- genuine v1 checkpoint fixture written by the actual pre-hardening
  save_wrapper (git main) + full migration/round-trip test
…xture, evidence runner

- MANIFEST.in prunes tests/ from the sdist (it shipped a partial test tree)
- scripts/smoke_test.py: post-install smoke for an installed wheel, run from
  outside the repo (import/version, model build, training+inference forwards,
  deterministic calibration, v2 round-trip, v1 migration, staged==eager,
  benchmark smoke)
- tests/fixtures/v1_cifar_resnet20.pth: genuine pre-v0.3 checkpoint written
  by the historical save_wrapper, force-added past *.pth ignore with a
  provenance README (the committed migration test needs it on fresh clones)
- scripts/evidence_run.py: bounded seeded CUDA evidence experiment (CIFAR-10,
  explicit disjoint train/temperature/calibration/test splits, epoch caps,
  wall-clock budget, fair 3-model benchmark incl. static MobileNetV2
  baseline, JSON output; negative results preserved)
… + reproducibility

- SECURITY_REVIEW.md: checkpoint trust model (weights_only + structural
  validation), model-code execution assumptions, provenance of datasets/
  weights/the one committed fixture, subprocess boundaries, scan results
- docs/MIGRATION.md: v1->v2 checkpoint mapping table and code-level changes
- PR_BODY_DRAFT.md (not opened)
- README: ASCII architecture diagram with the training/calibration/routing
  separation, checkpoint-migration section, reproducibility commands
- fix evidence runner OOM on 6 GB GPUs (baseline trains at batch 32, models
  moved off-GPU between phases, batched eval at 64)
- docs/evidence/cuda_evidence.json + CUDA_EVIDENCE.md: seeded 2+2-epoch
  ResNet-18 EE vs full backbone vs static MobileNetV2 on identical test
  samples; per-head temperatures all converged and genuinely different;
  1.10x throughput with WORSE median latency and better tails, 1.15% test
  accuracy drop vs 1% calibration budget — negatives reported as-is
- README: evidence table with the honest interpretation; trained weights
  artifact stays local (gitignored)
- version 0.3.0 in pyproject.toml, earlyon/__init__.py, README badge and
  citation
- CHANGELOG: unreleased section dated as 0.3.0 (2026-07-22)
- RELEASE_NOTES_DRAFT.md finalized as the canonical v0.3.0 release text
  (limitations, migration, bounded CUDA evidence with negatives preserved)
- PR body header finalized
@sohams25
sohams25 merged commit 19e8072 into main Jul 22, 2026
3 checks passed
@sohams25
sohams25 deleted the claude/earlyon-release-readiness branch July 22, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant