The release registry

5 records for one model, across three runtime targets. Identity is the provenance tuple, not the version string — because a version is the label people re-point at new bytes.

A model release is a set of bytes plus the claims made about them. This page is the registry that holds both, rendered from the same records the conversion pipeline writes. The measurements behind it are in What a conversion gate is for and Same recipe, different bytes. The one model this holds has to run in three different places, so the harder requirement was never a record count — it is many targets from one source, which is § 04, and which took two attempts to get right.

§ 01 · Why a precision label matters to someone holding a phone

This is a litter detector. It is easy to read what follows as numerical housekeeping, so here is what each number costs somebody.

A volunteer logging trash

They photograph a pile, and the model suggests what is in it. A person confirms every suggestion, so what a conversion costs them is a different set of suggestions to work through, not a wrong record. Blind human labels over the same 200 fixtures say the two artifacts find litter in an image about equally often — 52.6% against 53.2% — and fire on the same four clean scenes. The 112 flipped detections cost this person no measurable accuracy. What they cost is stability: one photograph, through two artifacts a registry would otherwise call the same model, does not put the same suggestions in front of them.

An organisation filing a cleanup report

What a cleanup produces is a composition — what kinds of trash, in what proportion. Those totals go to funders and into municipal stormwater reporting. If a conversion quietly shifts composition, the report shifts with it, and nobody downstream can see that the model changed underneath them. Per category these two artifacts move by up to 12 points in both directions — on denominators of 8 to 109 images, which is too small to call a shift and too small to rule one out.

A robot that has to pick it up

The eventual point of the class label is a grasp. A film wrapper and a rigid bottle are not the same physical action. At that stage a class flip is not a mislabel, it is the wrong motion attempted on a real object — which is why class flips are counted separately here and never folded into a detection count.

None of these three is served by "the file is 30% smaller." They are served by knowing whether the smaller file makes the same decisions, by knowing what the difference costs when it does not, and by both answers being attached to the artifact instead of filed somewhere else. Here the difference turned out to be instability rather than damage — which is a reason to route a flip count to a person, not a reason to stop counting it.

§ 02 · The records
Version Record id Target Artifact At threshold 0.4 Status
v2.0.1-fp32 31486ca5ebe0 onnxruntime-node · cpu 120.0 MB — (this is the reference) Serving
v2.0.1-fp16 61538e6fbff6 onnxruntime · CPU 63.1 MB 202 → 203 · 19 flips · 6 class flips · 10/200 images Candidate · never deployed
v2.0.1-int8 c02f95d84568 onnxruntime · CPU 36.0 MB 202 → 202 · 112 flips · 19 class flips · 50/200 images Candidate · never deployed
v2.0.1-fp32 357df9832c8d tensorrt · cuda · TF32 off 118.6 MB 183 → 183 · 0 flips · 0 class flips Candidate · never deployed
v2.0.1-fp32 644e1873370d tensorrt · cuda · TF32 on 118.9 MB 183 → 183 · 2 flips · 0 class flips Candidate · never deployed

Five records, one model, three runtime targets. Three ONNX graphs measured under ONNX Runtime, and two TensorRT engines built from an ONNX export of the same checkpoint on an RTX 4070. The two engines differ only in whether the TF32 builder flag was set, which is not a target property and not something a filename can hold — they share a target exactly and are still two releases. § 04 is why the registry can now say that.

Read the int8 row twice. 202 → 202 is not agreement; it is 112 disagreements that cancel exactly. A registry that stored a detection count would have recorded this conversion as identical to the one it replaces. Those 112 were later scored against blind human labels and cost no accuracy in either direction — which is what this column is for. The record holds the disagreement; a person decides what it means. Detail in the 2026-09-08 update.

Then read the TF32-on engine's row the same way: 183 → 183 with two queries disagreeing, one in each direction, on one image. Same trap, different hardware, third occurrence. The two engine rows carry no image count because the record does not hold one: the TensorRT parity report has the field, but the two harnesses name their outputs differently and the automatic transcription helper is ONNX-Runtime-shaped, so these two records' parity blocks were assembled by hand and dropped it. This column renders what the record holds rather than filling the gap from a file beside it — which is the whole point of putting evidence on the record.

§ 03 · What the digest covers

record_id = sha256(canonical_json(identity)). The identity block holds the artifact hash and byte count, architecture and precision, the conversion (exporter, mechanism, opset, source hashes), the full IO signature including dtype per tensor, the class-list hash, both recommended thresholds, and the target.

The version string is deliberately outside the digest. A version is a label, and labels get re-pointed at new bytes — which is the exact failure this exists to catch. It is recorded and constrained instead: one (model, version) may name exactly one artifact hash for a given recipe and destination, and a second set of bytes under the same recipe and destination is refused. § 04 is why that qualifier had to be added, and then widened.

Two things are inside the tuple that people often leave out. Thresholds, because the same bytes recommended at a different operating point make different decisions and are a different release. And the target — runtime, version, execution provider, host, and for a GPU the compute capability, CUDA runtime and driver — because a measurement does not transfer across them. The target being in the tuple and not in the uniqueness rule is what § 04 is about.

§ 04 · One source, many targets — where this module contradicted itself

The target has always been inside identity(). The uniqueness rule was keyed on less: Registry.register refused any second record whose (model_name, version) already existed, whatever target it named. Those two statements cannot both be right, and the gap between them is not academic.

An ONNX artifact and the TensorRT engine built from it are two releases by identity — different bytes, different runtime, different execution provider, two different record ids — and were a VersionCollisionError by the uniqueness rule. So were two engines built from one ONNX under different builder configurations, which differ by nothing a filename can hold. That is precisely the one-source-many-targets case the target matrix produces, and precisely the case a release registry exists to hold. This one could not hold it.

It was found by trying it — building the engines on a GPU host and attempting to register one alongside the fp32 ONNX record already there — rather than by reading the module. The registry refused a release it was built to record.

The fix, and its second draft The first attempt keyed uniqueness on (model, version, target‑digest). That fixed the ONNX-versus-engine case and missed the same contradiction one level down: two engines built from one ONNX with TF32 on and TF32 off share a target digest exactly — same GPU, same driver, same runtime — because a builder flag is part of the conversion, not the destination. They still collided. Uniqueness is now keyed on release_key: the identity tuple with the artifact hash removed. The recipe and the destination, without the bytes.
What did not change The original guarantee is intact. Two records sharing a release_key were produced the same way for the same place, so if their bytes differ the registry still refuses — that is the S3-key-overwrite defect this rule was built for. Both directions are tested: two targets, and two builds differing only in a builder flag, each register; the same recipe twice with different bytes still collides.
The refusal that is a feature A rebuild from an identical recipe that produces different bytes is also refused, and that is deliberate. TensorRT plans measurably are not byte-reproducible: one graph and one config produced three different plans, three different sizes, and none bit-identical. So the recipe does not identify what shipped, and a second row claiming to be the same build would hide that. The refusal surfaces the non-reproducibility instead.
What is registered now Both engines, alongside the ONNX. One checkpoint is now represented as an onnxruntime-node target, an onnxruntime target, and two distinct TensorRT builds that share a target and differ only in a builder flag — kept straight rather than forced into a semver bump or a forked model name, which were the only two workarounds available before the fix and both of which sever the registry's knowledge that these are one model.
The open item, named not fixed verify reports MISSING artifact for both engine records. The plans are ~119 MB each and deliberately not committed; dist/ is gitignored, and the non-reproducibility described in the build log is the argument that storing the blob would not establish identity anyway. verify cannot distinguish "this artifact is deliberately not retained" from "this artifact should be here and is gone" — and for a 119 MB plan the first is the normal case, so reporting it as MISSING is misleading. The fix is named: a record should be able to declare non-retention, and verify should report that as its own status. It is a schema change that would invalidate two records written against the current schema for no gain today, so it is written down rather than rushed.

Three narrower repairs came out of the same exercise.

  • Accelerator identity is fields, not a packed string. Target gained computeCapability, cudaVersion and driverVersion. A GPU target that packed those into hostClass would put them in the digest by string concatenation instead of as comparable fields. The two engine records carry all three — 8.9, CUDA 13.1, driver 591.86. They are omitted when absent rather than written as null, which is why the three ONNX record ids above still hash to exactly what they hashed to before.
  • "Cannot be inspected" is no longer stored as "nobody looked." § 05 reads precision off the graph's own weights. A TensorRT plan has no ONNX initializers to read — attempting it fails outright with a protobuf decode error — so PrecisionEvidence.opaque(reason) records why the evidence is missing. Both engine records carry "tensorrt plan: weights are not inspectable". Unobtainable evidence and absent evidence are different claims and now serialise differently. The consequence is stated rather than hidden: for those two records the "checked, not declared" guarantee does not apply, and their precision is back to being an unchecked assertion.
  • A parity report from another runtime is refused, not relabelled. Transcribing a report with no environment.onnxruntime used to stamp runtime: "onnxruntime" onto a measurement made under something else. It now raises instead of writing a false claim into an immutable record.

294 tests pass, 3 skip. .github/workflows/ci.yml re-hashes every record in the registry from its identity block on every push, so a hand-edited record turns CI red rather than waiting for whoever next reads the file.

§ 05 · Precision is derived, not declared

A record does not take the word fp16 from a filename. It reads the graph and counts weight bytes by tensor dtype, then infers a precision by majority. Which is how it can contradict a label rather than trust one.

It applies to 3 of the 5 records. The 2 TensorRT engines are serialized plans with no ONNX initializers to count, so they are not in the figure below — and their records say so in the field where the evidence would be, rather than leaving a null that also means "an ONNX nobody inspected." A guarantee that quietly stops applying is worse than one that visibly does not.

v2.0.1-fp32 FLOAT 113.9 MB v2.0.1-fp16 FLOAT16 57.0 MB v2.0.1-int8 INT8 27.9 MB + FLOAT 2.3 MB + INT64 32 B
INT8 FLOAT16 FLOAT INT64
Weight bytes by tensor dtype, all three inspectable artifacts on one scale. One colour per dtype, so the fp32 left inside the int8 graph is drawn in the same ink as the whole fp32 bar. The two TensorRT plans are absent because there is nothing in them to count.

fp16 comes back exactly half of fp32's weight bytes — 56,965,984 against 113,931,968 — meaning every weight converted and none were skipped. int8 does not: it is 92.5% INT8 with 7.5% of its weight bytes still fp32, tensors the dynamic quantizer declined to touch. So int8 is a majority, not a description, and the mechanism has to be in the identity tuple because the word alone does not identify what was built.

Version Dtype Weight bytes Share
v2.0.1-fp32 FLOAT 113,931,968 100.00%
v2.0.1-fp16 FLOAT16 56,965,984 100.00%
v2.0.1-int8 INT8 27,918,458 92.51%
v2.0.1-int8 FLOAT 2,259,112 7.49%
v2.0.1-int8 INT64 32 0.00%
§ 06 · The refusals, and that they fire

A check that never fires in a test is a comment. Each of these is asserted against the real artifacts, not a fixture.

  • Precision that disagrees with the graph. Registering the int8 bytes under v2.0.1-fp32 raises PrecisionMismatchError: declares 'fp32' but the graph's own weights are 'int8'. This is the case the serving contract cannot catch today.
  • One version naming two sets of bytes from one recipe. The second fp16 recipe under the existing v2.0.1-fp16, same target, raises VersionCollisionError. Under a different target, or from a different builder config on the same target, the same version is a second release and registers — that is § 04, and all three halves are asserted. The six TensorRT determinism builds are the case that still collides, and correctly: same recipe, same target, different bytes.
  • A release with no parity evidence. Parity is a required argument, and it is checked rather than trusted: the report must name this artifact's hash on the side it claims, must have been run at the operating point the record recommends, and for a conversion must have compared against the recorded source.
  • A hand-edited record. Rejected on read — the digest no longer matches the identity it covers. CI re-computes every record's digest from its identity block on every push, so this one fires in the repository and not only in a test.
  • A deleted assertion. Each refusal named here is grepped for in CI by the exception it raises. Removing the test that proves a refusal fires turns the build red, which is the only way "these checks are asserted" stays true a year from now.
§ 07 · What the records admit

A registry earns nothing by recording only the flattering fields.

Serving artifact measured on its target No. Its evidence came from Python onnxruntime 1.29.0 on a workstation; it serves under onnxruntime-node 1.27.0 on arm64 Lambda. The only records whose measuredOnTarget is true are the two ONNX candidates, which have never shipped — and there that flag is close to tautological, since the declared target names the only host those bytes have ever run on.
Source checkpoint hash null on all 5. It was never recorded anywhere, so provenance stops at the ONNX.
One source, or two? Two, and the records say so. The three ONNX records descend from a59fc417; the two engines record their source as c055e87c — a re-export of the same checkpoint from the same machine, five weeks later, with different bytes. That second ONNX has no record of its own. So "one model across three targets" is true of the checkpoint and not true of the bytes, and with the checkpoint hash null there is nothing in the registry that ties the two lineages together.
Candidates measured under identical conditions No, and now not even close. The fp16 run was on macOS 26.5.1 and the int8 run on 26.6.2 — same runtime and provider, different host build. The two engines were measured on Windows 11 against an RTX 4070 under TensorRT, which is not comparable to either. Recorded rather than smoothed over.
Anything promoted on this evidence Nothing. fp32 still serves. Four candidates exist and none has been promoted, and the gate that would enforce a decision is wired but has never run against real artifacts — it needs credentials and the fixture corpus first.
Precision checked on every record No — on 3 of 5. The two engines are opaque plans, so their fp32 is a declaration nothing verified. That is recorded in the record rather than implied by its absence, but it is still a weaker claim than the other three carry.
Artifacts retained Not all of them. verify reports MISSING for both engine plans, and four of the five artifactPaths point inside a gitignored dist/, so a fresh clone would report the same for the fp16 and int8 candidates. For the ~119 MB plans that is the intended state and the tool cannot yet say so — the open item in § 04.

These records describe 5 artifacts of one model, measured across a macOS workstation and a Windows desktop GPU. They license nothing about another architecture, another accelerator, an embedded board, or the arm64 host the model actually serves on.