The Horace experiment on the ET-SoC-1

aifoundry2's card, 20–21 September 2026 · fp32 TensorFMA on all 1,024 minions · workloads/sparsity --values, tools/ettelem, rtl-sim/fma_toggle · fourth version: strict temperature control, heating per FLOP, a power model from RTL switching activity, the speed effect, and now ten-minute runs, a model from flips to temperature, and structured matrices priced before they ran

Horace He's post "Strangely, Matrix Multiplications on GPUs Run Faster When Given 'Predictable' Data!" (April 2024) found that an A100 multiplies matrices of zeros about 15% faster than random ones (295 against 257 TFLOPS): predictable data flips fewer transistors, draws less power, and a power-limited GPU then clocks higher. On this chip the whole chain can be taken apart, because the RTL of its multiply-add unit is open. At the same clock and the same 9.2 TFLOPS, a matmul draws 38 W on zeros, 47 W on ones and 63 W on random values, and heats the die in proportion: 1, 27 and 76 thousandths of a degree per trillion FLOPs. Simulating the chip's own multiply-add RTL on the operands the card ran explains the watts: register bits clocked and nets toggled predict the board power of 14 patterns to ±0.5 W, including six patterns whose power was written down before they had ever run. A two-stage thermal network then turns watts into the temperature curve to about 0.3 °C, the resolution of the sensor. And from a cool die Horace's speed effect appears too: zeros run at 11.6 TFLOPS and random values at 9.3, because the firmware's governor lets only the cool pattern keep the fast clock. Run for minutes instead of seconds, nothing but zeros lasts: random data takes the die from 80 to 90 °C in 19 to 26 s, ones in about two minutes. A three-line model (flip rates to watts, leakage that grows with temperature, a thermal network) predicts those times from the flip counts. On runs it was not fitted to, the median error is 9% and the worst 23%; at ten minutes it runs 3 to 5 °C hot. It priced 14 structured matrices, from Hadamard to kaleidoscope, to 0.9 W before they ran.

Animated chart: die temperature over 7 seconds for matmuls on zeros, ones and random data, several runs each. Random runs rise fastest and cluster together.
Five runs each of zeros, ones and random normal matrices, every run launched as the die cooled through the same temperature. The thin staircases are the sensor's whole-degree readings; the smooth lines are the temperature recovered from the step times (section 2). The random runs use five different random matrices and still land on top of each other. To repost: horace-heating.gif (three kinds, 0.5 MB) or horace-heating-6.gif (six kinds, 0.9 MB).

1. Same FLOPs, different heat

All 1,024 minions repeat one 16×16×16 fp32 TensorFMA. Every pattern takes 546.0 cycles per op at 600 MHz, so every run does the same 9.18 × 10¹² FLOPs per second, 6.7 × 10¹³ in its 7.3 s. Only the values in A and B differ. Six patterns ran five times each; eight more ran twice each for the model in section 3.

Every run of the six main patterns. Top: die temperature; thin staircases are the sensor's whole-degree readings, one per run, and bold lines the mean recovered temperature. Bottom: board power, one thin line per run. Click a pattern to hide it.
Heating per FLOP: temperature gained over a 7 s run, divided by the FLOPs done in it

2. Strict temperature control

Two things make temperature a confound on this card. Power at the same work rises with die temperature, about 0.8 W per °C on the board (leakage). And the firmware's clock governor has a 65 °C threshold: below it a busy card is clocked up to 800 MHz at 0.62 V, above it the card is held at 600 MHz and 0.52 V (section 6). So every run starts from the same thermal state, well above that threshold:

  1. Heat. If the die is below 84 °C, 2 s bursts of random-data matmul bring it there.
  2. Cool along the same curve. The card idles until the mean of the 34 minion-shire sensors first reads 80 °C. Approached from above, that reading is an edge, not a range: the kernel launches within a second of the 81→80 step.
  3. Run one pattern for 7 s, with 10 Hz telemetry throughout. Each process holds the card for under 10 s.

Four burn-in cycles come first so that the heatsink reaches its periodic state. Patterns then run in shuffled blocks, one run of each per block, and the random patterns get new random tiles in every block: 46 measured runs and 4 burn-in cycles in 58 minutes. Three checks that the control worked:

The limit that remains is the sensor: it reports whole degrees. A run's rise read off the sensor is good to about half a degree, so the rises in this report are de-quantised: for each run, one number (the heating power) is fitted so that the thermal network of section 4, started from the launch state and rounded like the sensor, reproduces the run's readings. The times at which the reading steps from one degree to the next carry the information. Power needs none of this: it reads in 10 mW steps.

3. From flips to watts

The power differences come from one place: the fused multiply-add units, eight per minion, one per vector lane. The open RTL of this core family (core-et) includes that unit (txfma_top, seven pipeline stages) and the control around it. rtl-sim/fma_toggle runs eight copies under Verilator on the exact operand tiles the card ran (the host dumps them), in the order the tensor state machine issues them: for each row of B, for each row of A, two micro-ops of eight c = a·b + c; 512 micro-ops per op. Every result is checked: all 8,192 multiply-adds of two random-data ops match software bit for bit. Three things in the RTL decide the activity:

The table gives the counts per op for one minion, from the card's own tiles. The model is their weighted sum, with one non-negative energy per kind of event, fitted to the 14 measured powers:

board W = constant + e₁ · register bits clocked + e₂ · toggles in the multiplier tree + e₃ · toggles in the rest of the unit + e₄ · operand-word toggles outside the unit

Board power of every run at the start temperature (dots), and what the activity model says for that pattern when fitted without it (black tick)
Where the model puts each pattern's watts, above the constant part. Black line: measured.
Power predicted before the pattern had ever run, W (error against the measured seconds 1–3), by model form. Every form also has the clocked-bits term.

The test that matters most is above. Six of the patterns were invented for this version, and their power was written down before any of them had run under the power sampler (predictions_before.json, 06:50 on 21 September; the first of them ran at 06:59). The predictions came from models fitted to the 20 September data, nine patterns with no single random field and no single random operand among them, and from tiles drawn in software. The two model forms that split the toggles were right to 1.3 W rms. The two that do not were wrong by 2.7 and 4.2 W, and by 7.3 W on the pattern that separates them best.

4. From watts to degrees

The die's temperature follows power through a thermal network. A Foster chain (RC stages in series) with time constants fixed on a grid from 1.5 s to 1,000 s was fitted to the whole hour: board power in, sensor reading out, resistances held non-negative. Two stages carry everything this session can see.

Die temperature over the whole session: sensor reading (blue) and the thermal network driven by measured board power (orange)

A watt added to the board raises the sensor by 0.10 °C within a couple of seconds (the 1.5 s stage: die and spreader) and by another 0.12 °C over the next half minute (the 10 s stage: package and heatsink base). That is 0.06 °C per W after 1 s, 0.12 after 3 s and 0.16 after 7 s. The slower stages get nothing only because this protocol holds the hour's average power constant; the 20 September session, which did not, put a further 0.11 °C per W at 60 s and beyond. The residual over the hour is 0.28 °C rms, which is what rounding to whole degrees alone would leave (0.29 °C).

With both steps in hand, the chain runs from the RTL to the thermometer with no measurement of the run in between. For each run the network starts from the state the hour's measured power had left it in at launch. From there it is driven by the activity model's power for that pattern (from the fit that left that pattern out), plus 0.81 W for every degree the simulated die has gained: leakage, measured from the drift of power within the hot runs.

Temperature rise during a run. Thin: mean of the whole-degree readings. Bold: the recovered temperature. Black dashes: predicted from RTL activity alone.
Step 1. Activity to power, all patterns
Steps 1 and 2. Activity to temperature rise
Heating power over the pre-launch level, three ways: predicted from RTL activity (diamond), measured electrically (dot), inferred from the temperature trace alone (ring)

The ring in the last chart uses no electrical measurement at all. The sensor's whole degrees hide the size of the rise, but the instants at which it steps are sharp. So for each run one number was fitted: the added power which, put through the network from the launch state and rounded like the sensor, reproduces that run's readings. For the eleven patterns that heat the die by more than a degree, this thermal estimate agrees with the electrical one to 1.0 W rms, with a slope of 0.97 between them: the temperature rise is proportional to the power, and the power follows the counted flips. For the three coolest patterns (2 to 5 W over idle) the reading never leaves 81 °C during a run. That only bounds their rise to below about half a degree; the network predicts 0.3 to 0.8 °C.

5. Where the model is off, and why

  1. Most of the power is not flips of this computation. Of random normal's 63.4 W, 38.6 W is the model's constant: what the card draws at 80 °C with every multiply-add gated. Idle is 36.3 W of it: leakage (the same idle card draws 26.7 W at 62 °C), always-on clocks, DDR, PCIe and regulator losses. The other 2 W is 1,024 tensor state machines stepping through gated micro-ops. Temperature rises with power over the idle level, so heating per FLOP runs from 1 to 76 m°C while energy per FLOP only runs from 4.2 to 7.3 pJ.
  2. Register clocking is switching that the data does not control, except by being zero. A constant costs 8.4 W over zeros with almost no net toggles. "Proportional to flips" holds only once clock edges delivered to registers count as flips.
  3. A counted toggle is not a fixed charge. The fitted energies differ 25-fold between the multiplier tree and the rest of the unit. RTL net counts stand in for capacitance only within blocks of similar structure; a netlist with extracted capacitances would replace three fitted numbers with none. The zero-delay simulation also has no glitches. Real carry-save trees glitch, more so on random data, and the fitted energies absorb the average.
  4. Leakage feeds back. Random normal reads 65.8 W in seconds 1 to 3 and 68.0 W in the last two seconds of the same run: +2.2 W from its own 5 °C. The model's watts are at the launch temperature, and the chain adds 0.81 W per simulated degree. Without that term it would under-predict the hot patterns' rise by about a tenth.
  5. The sensor. One whole-degree number stands for the mean of 34 sensors. The thermal fit's residual is the rounding. The launch temperature is known to a few tenths of a degree in absolute terms, the same for every run, which matters only for the three coolest patterns. The hot spots are the multiply-add units, not the sensors, so the junctions that leak are hotter than the reading.
  6. Board power is not die power. About 7 of random normal's 30 W over zeros never reach the die; they are lost in the regulators. The network was fitted to board power and absorbs that as fewer °C per W. It would not transfer to a workload that loads a different rail.
  7. What is left. Power: 0.5 W rms out of sample, 0.9 W at worst. Rise: 0.33 °C rms from the RTL alone against 0.30 °C from the measured power. So almost all of the temperature error belongs to the thermal step and the sensor, not to the flips.

6. The speed effect appears on a cool die

Earlier versions of this report said the clock never moves on this card. That was wrong, and the reason is the point of this section. The service processor runs a governor (thermal_pwr_mgmt.c): while a kernel is running it steps the minion operating point down if the die is above a software threshold (65 °C) or the board's average power is above the TDP level (65 W), and up otherwise. This card's operating points are 600 MHz at 0.52 V, 700 MHz at 0.57 V and 800 MHz at 0.62 V. Every run on 20 September started above 72 °C, so the thermal branch held 600 MHz throughout, even at 70 W. After a night idle the die sits at 62 °C, and the same kernels behave differently:

Minion clock during 7 s runs started from a cool die (62–64 °C)

So on this card predictable data does run faster: 25% for zeros against random values, where Horace measured 15% on an A100. The reason is the one he gave: fewer flips, less power, and a governor that then allows a higher clock. Here the limit that bites first is thermal rather than electrical, and it only applies to a die that starts below 65 °C. A card that has been busy idles above that (72 °C on 20 September, 80 °C in section 1), runs everything at 600 MHz, and shows the effect as heat instead of speed. These seven runs are a demonstration, not a controlled experiment: their start temperatures differ by up to 2 °C, and a die that has just run does not return to 62 °C for many minutes.

7. Minutes instead of seconds

Seven seconds show the first two stages of the heating. To see the rest, the same patterns ran as single processes for up to ten minutes each (the lab's 10-second rule was waived for this), from the same start: heat to 84 °C if below, idle until the sensor first reads 80 °C, launch. A run ends early when the die reads 90 °C, which keeps it inside what this card had already seen (93 °C, 70 W). 29 runs took four hours, because after a hot run the die needs up to six minutes to get back to 80 °C. Some runs used fewer minions per shire, which scales the flip rate without changing the flips themselves.

Animated chart on a log time axis: die temperature of long matmul runs. Random data reaches 90 degrees in about 20 seconds, ones in two minutes, zeros never.
Zeros, ones and random normal at full load, and random normal on 384 of the 1,024 cores, on a log time axis. To repost: horace-long.gif (0.3 MB).
The long runs: die temperature against time since launch, log scale. Solid: the sensor. Dashed: the fitted model's curve from the run's flip counts (section 8). Choose which runs to show.

8. A model from flips to temperature

With the long runs the chain of sections 3 and 4 closes into one model that needs only the flip schedule. It has three lines:

flips   F_j(t) = N_j(pattern) × 1.10×10⁶ ops/s × active minions        (four kinds j, from the RTL)
power   P(t)   = 12.6 W + 23.3 W · e^((T−80)/36) + 1.85 W · active/1024 + Σ_j e_j · F_j(t)
heat    T(t)   = 22.8 °C + Σ_k x_k(t),    τ_k · dx_k/dt = R_k · P(t) − x_k

e_j   3.18 fJ   per register bit clocked
      0.025 fJ  per net toggle in the multiplier tree
      0.80 fJ   per other net toggle in the unit
      15.5 fJ   per operand-word bit toggled outside the unit
R_k   0.106 °C/W at 1.5 s · 0.050 at 4 s · 0.234 at 60 s · 0.136 at 150 s
      0.860 at 400 s · 0.081 at 2,500 s                       (sum 1.47 °C/W)

The power line has a fixed part, leakage that grows exponentially with die temperature, the tensor state machines of the active minions, and an energy for each kind of flip. It was fitted to every 10 Hz sample of five hours of sessions: the leakage to the idle samples (rms error 0.20 W from 64 to 88 °C), then the flip energies to the busy samples (0.49 W). The flip energies agree with section 3, which used only seconds 1 to 3 of 46 short runs. The heat line is the thermal network of section 4 with its slow stages now visible; one more number pins the slowest of them, the 62 °C at 26.7 W where this card had settled after a night idle. Driven by the measured power it follows the sensor to 0.63 °C rms over the five hours.

Leakage: idle board power against die temperature, all sessions, with the fitted exponential
Heat: degrees per watt after a step of switching power

Leakage is two thirds of an idle card's power at 80 °C (23 of 36 W) and it is what makes temperature so sensitive to flips here. Each degree adds 0.65 W, and each watt, given ten minutes, adds 1.2 °C through the network. Closed on itself, that loop has a gain of 0.95 at 80 °C: a watt of switching held for ten minutes raises the die by 3.1 °C, not 1.2, and the gain passes one at 82 °C. The table turns that into degrees per kind of flip for the random fp32 matmul.

How well it fits. For each long run the network is started in the state the measured history had left it in at launch, and then driven by the power line above, with the run's flip rates and its own simulated temperature; nothing measured during the run goes in. The dashed lines in the chart of section 7 are these curves. For the 19 runs that reached 90 °C the time is off by 12% in the median (23 s for 26 s measured, 122 for 119, 326 for 315); the worst is the half-zero random matrix, 157 s for 107 s. For the seven ten-minute runs the end temperature is off by 2.5 °C rms. These are fit numbers: the network and the leakage were estimated from the same session.

How well it predicts. So every parameter was fitted again on the first 7,400 s of the long session (15 runs, plus the 7-second session of section 2) and frozen, and the 12 later runs were predicted. Nothing is fitted on the held-out part: the thermal state at each launch comes from an observer that has seen the telemetry up to that launch only (tools/ettelem/validate_flip_model.py). The held-out runs include four patterns and two core counts that the thermal fit had never seen.

The long session: sensor reading (blue) and the thermal network driven by the measured board power (orange)
Seconds to 90 °C, run by run, from flip counts against measured. Dots: runs the model was fitted on. Rings: held-out runs (the second half of the session, and section 9's new matrices)

A predictor for custom workloads. tools/ettelem/predict_heat.py wraps the three lines: give it the A and B tiles of a workload (or a pattern name), the number of active minions and a start temperature, and it replays the tiles through the RTL, counts the flips, and prints the switching watts by kind of flip, the board power, the temperature after 10 s, 1 min and 10 min, the time to a cap, and the duty cycle that would hold the start temperature. Section 9 tests it on matrices it had never seen.

Where the die settles if a switching power is held for hours, on this card's cooling

The flip budget of this card. Setting the heat line to its steady state gives the curve above: for each switching power, the temperature at which the die would settle (solid) and the temperature above which it runs away (dashed). The two meet at +3.1 W of switching, with the die at 82 °C: beyond that there is no equilibrium on this card's cooling, only a time to 90 °C. The long runs agree: 2.7 W (ones on 256 cores) cooled, 3.4 W (random on 128 cores) sat at 81 to 82 °C for ten minutes, 4.2 W (the checkerboard) crept to 87 °C. In flips, 3.1 W is about 10¹⁵ clocked register bits per second, or a full-load random matmul 11% of the time. The thermal resistance behind this, 1.47 °C per W, belongs to this card in this desktop chassis, which idles at 62 °C; the flip energies and the leakage belong to the chip.

9. Custom workloads: structured matrices

The point of the model is to price a workload before running it. tools/ettelem/make_tiles.py generates structured 16×16 operand pairs: Hadamard and DCT matrices, the cosine and sine halves of a DFT, butterfly factors and their dense kaleidoscope products (the family that generalises the fast Fourier transform), identity, banded, block-diagonal, triangular, rank-1, circulant and 4-bit quantised matrices, a weights-times-ReLU pair, and a matrix of negative zeros. Their tiles went through the RTL bench and the three lines of section 8, and the predictions were written down at 13:08 on 21 September (structured_predictions_before.json), before any of them had run. Fourteen then ran twice each under the strict protocol of section 2.

Board power at the launch temperature: predicted from the tiles alone, before the first run, against measured

And the heat, not just the watts. Five of the structured matrices then ran as long runs in a separate afternoon session (section 7's protocol), with the model frozen as fitted that morning, before they existed on the card. Nothing was fitted on that session: the thermal state at each launch comes from the observer of section 8, from telemetry before the launch only.

Four of the six times to 90 °C are within 3%, and the five-minute run's end temperature is off by 0.2 °C. The DFT pair is wrong by two thirds, for the same reason as its watts: 2.8 W missing out of 13 W of switching costs 47 s on a die this close to runaway. The ReLU pair is a third slow for a 1.2 W miss. Near the card's flip budget the time to a cap is a sensitive function of power, so a watt of model error matters; at full random load it does not (19.5 s predicted, 19.1 s measured). With the first-half model of section 8 instead, the same six come out at 7% in the median and 65% for the DFT pair.

$ python3 tools/ettelem/make_tiles.py kaleidoscope k.bin
$ python3 tools/ettelem/predict_heat.py --model docs/reports/data/2026-09-21-horace-aifoundry2/model.json --tiles k.bin
flips per op and minion: 4096 of 4,096 multiply-adds valid; 2.47 M register bits clocked; 74.3 M tree toggles; 13.27 M other toggles; 140 k operand-word toggles
switching power: register clocking 8.8 W, multiplier tree 2.1 W, rest of the unit 12.0 W, operand words 2.4 W, state machines 1.9 W; total 27.2 W on 1024 minions
board power at 80 C: 63.1 W (idle 35.9 W)
die temperature from an idle die at 80 C: 10 s: 86.5 C; reaches 90 C after 27 s
to hold 80 C indefinitely the card can shed 3.1 W of switching: run this workload 11% of the time

10. Method

cmake -S workloads/sparsity -B build/sparsity -DCMAKE_PREFIX_PATH=/opt/et -Wno-dev && cmake --build build/sparsity -j4
cmake -S tools/ettelem -B build/ettelem -DCMAKE_PREFIX_PATH=/opt/et -Wno-dev && cmake --build build/ettelem
tools/ettelem/run_horace_strict.sh build/horace3 80 84 4 5 2 7 "zeros ones pi sparse50 uniform randn" \
    "signs pow2 mant a_randn_b_ones a_ones_b_randn ternary sparse75 checker"        # about 70 minutes
make -C rtl-sim/fma_toggle && python3 rtl-sim/fma_toggle/toggles.py --tiles-dir build/horace3/tiles --out toggles.json \
    --patterns zeros,ones,pi,sparse50,uniform,randn,signs,pow2,mant,a_randn_b_ones,a_ones_b_randn,ternary,sparse75,checker
python3 tools/ettelem/analyze_horace_strict.py build/horace3 --toggles toggles.json --out horace3.json
python3 tools/ettelem/make_heating_gif.py horace3.json horace-heating.gif
tools/ettelem/run_horace_cold.sh build/horace_cold 63 300 zeros randn ones randn zeros ones   # needs a die that has idled to 62 °C
tools/ettelem/run_horace_long.sh build/horace_long tools/ettelem/horace_long.sched 80 84 90        # up to 10 min per run, 90 °C cap; 4 hours
python3 tools/ettelem/make_tiles.py --all build/structured_tiles                                   # structured operand pairs
tools/ettelem/run_ablation.sh build/ablation1 tools/ettelem/ablation.cfg 2 7                       # strict 7 s runs of any configuration
tools/ettelem/finish_horace.sh                                                                     # analyses, model, GIFs, both reports
python3 tools/ettelem/predict_heat.py --model docs/reports/data/2026-09-21-horace-aifoundry2/model.json --tiles my.bin

Raw data: docs/reports/data/2026-09-21-horace-aifoundry2/ (runs, start records, compacted 10 Hz telemetry, the operand tiles, activity counts, analyses, the model, its predictions made before the runs). The two earlier versions of this experiment (20 September: an uncontrolled run, then runs started at or below 80 °C) are in docs/reports/data/2026-09-20-power-aifoundry2/; their powers agree with these to within 1.5 W, and their statement that the clock is fixed is corrected in section 6.