# What changed in the instrument for this board — 2026-09-21

**What this file is about, for a reader who scrolled straight to it.** This directory is the
language half of a GPU bench that has now run on five boards with one instrument: two GeForce
RTX 3080 10 GB in a pair, one RTX 3090 24 GB, one RTX 3090 Ti 24 GB, one RTX 3080 Ti 12 GB, and
now an **NVIDIA GeForce RTX 5090 Laptop GPU 24 GB** in the machine this file is committed from.
The whole value of that sentence rests on the instrument being the same one — so every edit made
to it for this board is listed here, with the digest before and after, rather than left for a
reader to find by diffing two ninety-kilobyte files.

**Forked from** `bench/gpu-3080ti-benchbox-2026-09-18/` on 2026-09-21. That leg came from
`gpu-3090-desktop-2026-09-17/`, which came from `gpu-2x3080-2026-09-16/` and before that
`gpu-3090-vs-6000-2026-09-16/`.

**Why that leg and not the 3090's**, since the 3090 is the 24 GB bench this one actually
compares against: it is the only leg whose `benchlib.py` reads its card out of the environment
rather than hardcoding a UUID, and it carries the later hardening — `params.sh`, `stage.sh`,
`progress.py`, `handoff.py`, the cap read-back's row-count check, a much larger test file. The
24 GB **arm semantics** were then brought back from the 3090: one arm, the strict `whole` fit
rule, both bank models held entire.

---

## The claim, and its limits

⚠ **There is no byte-identity fence on this half of the bench, and there never was.** The
render half's `renderlib.py` is byte-identical across every leg and a test asserts it by digest.
On the language side each leg carries its own `benchlib.py` and its own `bench_gpu.py`, and
their digests differ leg to leg — checked, four legs, four digests. So the strongest claim
available here is weaker, and it is stated as the weaker thing it is:

| file | claim | fence |
|---|---|---|
| `bench_concurrency.py` | byte-identical to the source leg | `tests/test_params.py` |
| `bench_doorman.py` | byte-identical to the source leg | same |
| `bench_embed.py` | byte-identical to the source leg | same |
| `bench_idle.py` | byte-identical to the source leg — and to the 3090's and the 3090 Ti's, which all three already shared | same |
| `retherm.py` | byte-identical to the source leg | same |
| `verify_card_mapping.py` | **one change**: the receipt names the board it is about (`target_uuid` / `card_expected_uuid` at the top level). The dry run asked for it — the checker reads a record's card from those keys and this file carried the UUID only inside its per-shape list, so the residency receipt, the one file whose entire subject is *which card*, came back as `card (none)`. | — |
| `benchlib.py` | **changed**; every change listed below | this file |
| `bench_gpu.py` | **changed**; every change listed below | this file |

| file | source leg (`gpu-3080ti-benchbox-2026-09-18`) | this leg |
|---|---|---|
| `benchlib.py` | `56c96d7fb8adc1b2…` | `143fb9bb8695b3eb…` |
| `bench_gpu.py` | `c70b5fb55b824c88…` | `595258bdf0a6aa51…` |

---

## `benchlib.py` — four changes, 208 diff lines, none of them in a measuring path

### 1. The cap is read from **two** nvidia-smi fields, and every record says which

`power.limit` is NVML's power-*management* limit — what `nvidia-smi -pl` writes. On a board
where one has never been **set** it answers the literal string `[N/A]`, and this board is such
a board. Read 2026-09-21T14:52Z, same call as `date -u`:

```
power.limit          [N/A]
enforced.power.limit 150.00 W
-q -d POWER          Current 150.00 W · Default 95.00 W · Max 175.00 W
```

A gate reading `[N/A]` does not fail loudly — it yields an empty value that propagates into a
table cell, which is this bench's own named recurring defect. So:

* `SMI_FIELDS` and `SMI_NUMERIC` gain `enforced.power.limit`; both are sampled at 2 Hz.
* New: `CAP_FIELD_PREFERRED` / `CAP_FIELD_FALLBACK`, `cap_reading(row)`, `cap_fields(row)`,
  `cap_on_card(uuid)`.
* `_reduce_rows` writes three keys instead of one: `power_limit_w`, **`power_limit_field`**,
  `power_limit_enforced_w`.

**The order is a provenance rule, not a taste.** `power.limit` wins when it answers, because it
is the field every earlier bench of this ladder read, so a box where both answer stays
byte-comparable with the 3090, the 3090 Ti and the two 3080s. A record that can read **neither**
carries `None` and the gates refuse on it — never a default.

### 2. The bytes-per-second ceiling is **withheld** on a GDDR7 board

The harness derives a peak bandwidth from `clocks.mem` × bus width × **two bits per clock**.
Two bits per clock is exact for GDDR6 and GDDR6X and is *checked* against three published
bandwidths in the test suite. It is not a fact about GDDR7, and this is a GDDR7 part on a
256-bit bus (the width READ from NVML per UUID, not typed).

Applying the constant anyway would have produced a confident GB/s number in every cell of this
board's bandwidth column, derived from a rate nobody here measured — a specification wearing a
measurement's clothes, which is exactly what this bench refuses elsewhere. So `DATA_RATE_PER_CLOCK`
became a registered lookup, `MEMORY_TECH_BY_BOARD`, with an explicit unregistered case:

| board | technology | bits per clock |
|---|---|---|
| RTX 3090 Ti / 3090 / 3080 Ti / 3080 | GDDR6X | 2 |
| **RTX 5090 Laptop GPU** | **GDDR7** | **None — withheld, with `DATA_RATE_UNREGISTERED_REASON`** |
| anything unlisted | unknown | None |

The three `peak_mem_bandwidth_gbs_at_*` cells become `None` and the record carries
`peak_mem_bandwidth_withheld_because`. **The memory CLOCK is a reading and prints unchanged** —
only the derived ceiling is withheld. `bandwidth_formula_check()` still reproduces all four
published bandwidths inside 0.5 %, so the formula did not drift; it was scoped.

### 3. `UPS_NAME` reads the environment, and the absence has a sentence

It was the literal `"pr1500@localhost"` in five files across this ladder — a box with no UPS
failed *soft*, which is the easier failure to ship past. It is now
`os.environ.get("BENCH_UPS", "pr1500@localhost")` (the same shape `renderlib.py` already used
for it), and `UPS_ABSENT_REASON` carries what was asked and what answered on this box.

### 4. Nothing else

No change to any decode path, any timing path, any gate threshold, any fit rule, the frozen
prompt or its sha check, the spread gate, the contention gate, the stop temperatures, or the
2 Hz tracer. `STOP_CORE_TEMP_C` is still 83.0 and `CONTENTION_MAX_UTIL_PCT` is still 5.0 — on a
box that is *not* quiet, and deliberately: the gate is left strict so it refuses rather than
being loosened.

---

## `bench_gpu.py` — three changes, 327 diff lines, all configuration

### 1. `ARMS` — the 24 GB single-card shape, scored under the **strict** rule

The source leg's arms were `card` (12 GB, `any-vram`, because nothing in the bank fits it),
`cpu-box` and `control-3090`. This leg's are:

* **`one`** — the soldered RTX 5090 Laptop GPU, `fit: "whole"`. 24 GB holds both bank models
  entire, so this arm is scored under the **same strict gate the one-3090 bench used**: every
  byte on the card or the rung does not count. That is what makes a laptop figure comparable to
  a 3090 figure at all; quietly inheriting the 12 GB legs' looser gate would have published a
  spilled number under a whole one's heading.
* **`cpu-box`** — registered, **not on the run list**. This machine already has fourteen
  published CPU-only rows under the roster identity `rig-b-the-laptop`, taken on this very
  machine with, in that shelf's own words, *"a 24G-VRAM-class GPU present and parked"*. The
  control exists; it is not re-measured. The arm stays defined so a later operator can run it
  without re-deriving the shape.
* `control-3090` is **gone**. There is no slot and no board to put in it.

`FIT_RULE_NOTE` and `LADDER_STOP_NOTE` are rewritten to state the one rule rather than the two,
and `CROSS_ARM_RATIO_NOTE` is replaced by `CAP_AXIS_NOTE` — see below.

### 2. `CAP_LADDER_W` — `(95, 175)`, with the run order stated separately

Both wattages READ off the card. `CAP_RUN_ORDER_W = (175, 95)`, because 175 W is reachable only
once `nvidia-powerd` is stopped and the operator's one paste does both in a single step.

`CAP_ABOVE_STOCK_NOTE` becomes a registered **not-applicable**: on a desktop board
`power.max_limit` sits above the default by a user's choice and is an overclock reading (the
3090 Ti's ruling: *"'Uncapped' = 450 W, not 480"*). On a mobile board it is reached by NVIDIA's
own Dynamic Boost during ordinary use, unasked — so max IS the ladder's top rung and an
above-stock door would open onto a rung the bench already runs.

`CAP_DYNAMIC_BOOST_NOTE` registers the posture that is **not** a rung: powerd running, no cap
set, the board floating 95–175 W. Its cap cell is a **range** derived from the 2 Hz trace and
never a single number.

### 3. `CAP_AXIS_NOTE` — the reason no cap-keyed cell can take a row from here

The 24 GB comparison tables key their columns on 250/300/350/400/450/600 W. This board's whole
envelope sits below the lowest of them, so a row from this bench fills **zero** cells in any
cap-keyed table however well it runs. The note says so, in the record, so a reducer that tried
could not do it quietly. Every scored file carries it.

---

## What is NOT in this leg, and why

* **No reducer.** See `REDUCER-NOT-YET.md`.
* **No `--above-stock` door.** Max is a rung here; the door would duplicate it.
* **No shortened-ladder fallback.** The source leg had four caps and an overnight window. This
  one has two rungs of about 32 minutes each and runs in the daytime with the operator at the
  keyboard: there is nothing for a shortened ladder to rescue, and carrying the machinery would
  leave a door by which this bench could publish a `-short` file nobody asked for.
* **No `bench_two_seats.py`.** One card.

---

# ADDENDUM — `benchlib.cap_fields` learns the posture and the range (2026-09-21, ~18:2xZ)

**What this addendum is about, for a reader who scrolled straight to it.** `benchlib.py` is this
leg's half of the measurement path — the sampler, the reducers, the cap reader — and this file
exists because, unlike the render half's `renderlib.py`, it is **not** byte-identical across legs
and so has no digest fence to lean on. Every change made for this board is listed with its digests.
**Window: the change landed 2026-09-21 ~18:2xZ UTC, after the operator's 17:20Z read-back and
before any scored arm of this leg had produced a file.** No published figure is affected, on this
leg or any other: this leg has no measured rows yet, and no other leg shares this file.

**Why it was needed.** The 17:20Z read-back — *"Changing power management limit is not supported
for GPU: 00000000:01:00.0"* — withdrew this leg's cap ladder and replaced it with two postures.
Under `dynamic-boost` the board's enforced limit **moves while a run is being scored**, and until
this change a scored record carried only `power_limit_enforced_w`: the **first sample** of the
window. On a fixed rung that is the cap. On a floating one it is one instant of a moving number,
and printing it in a cap column is the exact fabrication this leg is built to refuse.

**The change, in full — and it is ADDITIVE. No existing key changed meaning or value.**

* New module constants `POWER_POSTURE_ENV` / `_FIXED` / `_DYNAMIC` / `_NOTE`, and
  `power_posture()`, which reads `BENCH_POWER_POSTURE` from the environment and **defaults to
  `fixed`** — the strict side. A run whose wrapper forgot to declare a posture gets the reading
  that is right for a board standing still, and `capgate.sh`'s gates then refuse rather than
  relabel.
* `cap_fields(row)` becomes `cap_fields(row, rows=None)`. It still returns `power_limit_w`,
  `power_limit_field` and `power_limit_enforced_w` unchanged, and adds:
  * `cap` and `power_posture` — the posture this record was measured under, on every record that
    carries a cap, so no reader has to infer it from a file name;
  * `power_limit_enforced_stats_w` — min/median/mean/max **with its `n`**, reduced from the same
    2 Hz trace the watt column comes from (`None` when no window is passed);
  * under `dynamic-boost` only: `power_posture_note`, and a `cap_cell` string built here rather
    than by each reader, so two readers cannot round it two ways. **`min == max` renders as
    `"95 W, flat"` with a note calling it a FINDING — never as `95–95 W`.** An unreadable window
    renders as `cap_cell: null` with `cap_cell_withheld`, never as a number.
* `_reduce_rows` passes its window: `**cap_fields(rows[0], rows)`.

**What a reader can check.** With `BENCH_POWER_POSTURE` unset or `fixed`, `cap_fields` returns the
same three keys it always did plus `cap: "fixed"`, `power_posture: "fixed"` and the stats — and
**no `cap_cell` key at all**, because a fixed rung's cap cell is the number that is already there.

**The digests.**

| | sha256 of `benchlib.py` |
|---|---|
| as this leg was forked and as the 17:27Z run used it | `7362b89bce2e658f53e55c2e1c53ed6d6e91fb0902b128867190c4089f77f193` |
| after this addendum | `9d7d109933f1769c44d705ddc01ba3f50c234bebd0cebadab0b1eb56e61e7f95` |

**`bench_gpu.py` did not change in this addendum**, and neither did the sampler, the gates or any
scoring path. The only behaviour any earlier figure could have depended on is `cap_fields`'s three
original keys, and those are byte-for-byte what they were.

**The alternative that was considered and rejected:** leaving the per-run record alone and letting
the stage-level witness (`cap_witness.py`, 0.2 Hz) be the only range. It would have kept this file
frozen — and it would have meant that the cap beside a *scored* figure was a different reading,
at a different rate, from a different file, taken over a different window than the figure itself.
A watt column and its cap belong to the same trace.

---

# ADDENDUM 2 — `benchlib.power_posture` learns a THIRD posture, `shipped` (2026-09-21, ~23:2xZ)

**What this addendum is about, for a reader who scrolled straight to it.** `benchlib.py` is this
leg's half of the measurement path — the sampler, the reducers, the cap reader. Addendum 1 above
taught its cap reader two POSTURES after the board refused a power limit. This addendum adds the
third: **`shipped`** — `nvidia-powerd` running **and** this box's own boot-time GPU clock lock
(`ai-perf.service`, `nvidia-smi -lgc 1200,2550`) in force, which is what this laptop does on an
ordinary day. **Window: the change landed 2026-09-21 ~23:2xZ UTC, after both measured passes had
finished (the 95 W pass at 18:43Z, the dynamic-boost pass at 21:02Z) and before the third pass had
run.** No published figure changes: the two measured passes' result files are untouched, their
`cap` fields still read `fixed` and `dynamic-boost`, and this leg has no `shipped` row yet.

**Who asked for it.** `PREREG.md` closed the second pass with a registered QUESTION rather than a
ruling — Q-BOOST-1, *"which posture is 'how this laptop ships'?"* — because this box does not boot
with Dynamic Boost alone. an operator ruled it at ~23:10Z: *"roll with the rec"*, and the rec was a THIRD
PASS rather than an edit to either of the first two. `PREREG.md` Amendment 4 registers it.

**The change, in full — and it is ADDITIVE. No existing key changed meaning or value.**

* New module constants `POWER_POSTURE_SHIPPED`, `POWER_POSTURES_FLOATING` (the tuple of postures
  whose enforced limit MOVES: `dynamic-boost` and `shipped`), `POWER_POSTURE_SHIPPED_NOTE` and
  `POWER_POSTURE_NOTES` (posture → note). `POWER_POSTURE_NOTE` still exists and still describes
  the `dynamic-boost` posture; it now also states that that posture's clocks run RELEASED, which
  was true before and unwritten.
* `power_posture()` returns `shipped` for `BENCH_POWER_POSTURE=shipped` (or `ship`). Everything
  else is unchanged, **including the default, which is still `fixed`** — the strict side.
* `cap_fields` branches on MEMBERSHIP in `POWER_POSTURES_FLOATING` rather than on equality with
  `dynamic-boost`, so a `shipped` record gets the same `cap_cell` / `power_posture_note` /
  withheld-cell treatment. The flat-limit note names the posture it was measured under rather
  than the literal word `dynamic-boost`.

**What a reader can check.** With `BENCH_POWER_POSTURE` unset or `fixed`, `cap_fields` returns
exactly what it returned before Addendum 1 plus that addendum's keys, and **still no `cap_cell`**.
With it set to `shipped`, `cap["cap"]` and `cap["power_posture"]` read `shipped` and the `cap_cell`
is the same RANGE shape the boost posture gets — because the limit floats identically. The one
thing that differs between the two floating postures is a CONDITION, and it is not in this file at
all: it is read from the card by `clocklock.sh` and lives in the pass's clock-lock receipt.

**The digests.**

| | sha256 of `benchlib.py` |
|---|---|
| after Addendum 1, and as both measured passes used it | `9d7d109933f1769c44d705ddc01ba3f50c234bebd0cebadab0b1eb56e61e7f95` |
| after this addendum | `36b520d434e24e1009aa76aacc981f1e748283223619aba5f8f32008d5a57d94` |

**`bench_gpu.py` did not change in this addendum either**, and neither did the sampler, the gates
or any scoring path.
