September 2026
Inference-aware cooling: 29% less fan speed on the AMD R9700, same temperatures
A fan controller only knows the past: the heat that has already happened. The AI model running on the card knows the future: how many tokens are queued, how long the prompt is, whether an agent is about to call again. On the AMD Radeon AI PRO R9700 we let the model tell the fan what is coming, so the fan spins for the work ahead instead of the heat behind. Chat runs with the blower at its quietest setting, agent loops at 29% less fan speed, batch work at 25% less, at the same peak temperatures and the same tokens per second. No power cap, no undervolt, no slower clocks.
TL;DR
- Firmware fan control is late by design. It reacts to temperature, so it spins up after a chat request has already finished and keeps 3,500 RPM through the pauses of an agent session.
- The engine forecasts its own heat. Every second the server publishes how many seconds of full-power work lie ahead: the live request's remaining prefill and decode, the queue, and whether the session will continue.
- A learned thermal model picks the fan speed. Twenty-five features, ridge regression on 64 recorded runs, predicting the die 10, 30 and 60 s ahead within 2 C, which is the sensor's own noise floor.
- Same peaks, less fan, same tokens. Chat inaudible, agent loops −29%, batch −25%. Every pair of runs delivered the same number of requests at the same tok/s.
- Physics sets the floor. At 300 W this blower needs 2,700 to 3,100 RPM to hold 90 C. No software goes below that under a sustained load; software decides how often you are there.
- It fails safe. Corrupt hint, dead engine, killed service, missing model: six injected faults, the die never passed 90 C. Validated on the production board.
Why a fan controller is always late
The R9700 is a 300 W card with a 75 mm radial blower. Its firmware targets 80 C on the hotspot and pushes the fan to about 3,700 RPM whenever the die gets there. That policy is tuned for a server rack, and on a desk it produces the two sounds everyone knows: the pulse after every short request, when the fan ramps for heat that has already left, and the steady roar through an agent session, because the firmware cannot know that the pause between tool calls lasts eight seconds.
We recorded the stock behaviour on three synthetic traces that stand in for real use. Chat: two requests of 1,600 prompt tokens and 200 output tokens, then a 20 s pause. Agent: 30 s bursts of back-to-back requests with 8 s pauses. Sustained: back-to-back forever. Every run is six minutes, sampled at 1 Hz from the card's own sensors.
The physics you cannot argue with
Before building anything, we measured what the fan actually buys. With the card pinned at 300 W we capped the blower at fixed speeds and watched the die settle.
Two rules fall out of that curve. The first is a floor: under a sustained 300 W load, holding the die around 90 C needs 2,700 to 3,100 RPM, and nothing a controller does changes it. The second is why the floor is worth fighting for anyway: fan noise scales with roughly the fifth power of speed, so a 29% slower fan is about 7 dB quieter, half the perceived loudness.
The consequence for control is simple. Any second the fan spins faster than the die needs is wasted noise, and any second it spins slower than needed is a temperature overshoot the firmware will correct loudly. The only way to be right on both sides is to know what is coming.
Co-design: the engine forecasts its own heat
The inference engine has everything a forecast needs and a thermometer has none of it. So the server now publishes, once per second, a small JSON object: how many seconds of full-power work lie ahead.
busy_ahead = known + duty60s · max(0, 60 − known)prefill_left: prompt tokens still to compute, net of the prefix cache. rprefill, rdecode: the engine's own measured rates. L̂: the expected completion length, learned per request shape (thinking on/off, max_tokens bucket). duty60s: the busy fraction of the last minute, or an agent prior of 0.5 when the request carries tool definitions.
The first term is what the engine knows exactly: the request it is serving, its progress, and the queue behind it. The second term is a forecast of the session: an agent loop that kept the GPU busy 80% of the last minute will very likely keep doing so, and a request that arrives with tool definitions is an agent loop from its first token. A final answer without a tool call, or a client that disconnects, sets a session_over flag and the forecast drops to zero at once. Under concurrent serving the scheduler publishes the exact remaining work of every live slot. None of this reads a temperature: the engine stays portable, and the fan service owns the thermal side.
A thermal model at the sensor's noise floor
The forecast says how long the card will burn 300 W; the fan service has to say how hot the die gets, for a candidate fan speed, at the end of that horizon. We started with a hand-fitted two-node model, which is the physics: a fast hotspot term that follows power within seconds, and a slow heatsink node with a 45 s time constant.
dThs/dt = (Tss(P, n) − Ths) / τ, τ ≈ 45 s
Tss(P, n) = 9 + 43 · (P / 300) · √(3660 / n)P in watts, n the blower speed in RPM. The square root is the empirical fit of thermal resistance against airflow for this blower and fin stack.
That model got the shape right and the number wrong: on held-out runs it under-predicted the hot samples by 4 to 6 C at the horizons the controller plans over, which is exactly how much the die overshot in the afternoon runs. So we replaced it with a learned one, a ridge regression on 25 features built from the current state, its 5 s and 10 s lags, the ambient proxies the card exposes, and the plan under evaluation.
φ(x) = [1, Tdie, P, n, Tedge, Tmem, Tapu, lags, PH, nH, PH√(3660/nH), Pend, e−H/τTdie, prefill, …]PH and nH: mean power and fan speed over the horizon under the plan (300 W while busy, 20 W idle, the blower at the candidate floor). Pend: power at the horizon's end. prefill: the fraction of the horizon spent prefilling, because long prompts run hotter than decode at the same watts.
The controller is then one line. Every second it searches the fan duty from 50% upward and keeps the first one whose predicted peak stays under the target at every horizon the forecast reaches:
This is where the thermal mass gets used on purpose. A 30 s job from a cool die never reaches 90 C, so the fan stays at its floor and the job ends before the heat matters. A 60 s job does, so the floor goes straight to the level the end of the job needs, in one move, with no overshoot and no hunting. Long work settles at the physics floor.
Results
Everything below is the production service, the engine with the forecast, and a 92 C target, on one Lucebox with the R9700 and a Strix Halo, serving Qwen3.8-27B with its DFlash2 drafter. The target was chosen because it reproduces the stock firmware's peak temperatures.
| Workload | Stock blower | Co-design blower, mean / peak | Die peak | Requests, tok/s |
|---|---|---|---|---|
| Chat, 5 s bursts / 20 s pauses | 1,906 RPM, pulsing | 1,734 / 1,741, never leaves the floor | 73 C (stock 76) | 28, 65.6 (same) |
| Agent loop, 30 s / 8 s | 3,297 RPM | 2,343 / 2,647 | 88 C (stock 88) | 94, 65.1 (same) |
| Long outputs, 1,500-token answers | — | 2,595 / 3,044 | 90 C | 11, 50.4 |
| RAG, 6K-token prompts, unique | — | 1,852 / 2,078 | 86 C | 20, 18.1 |
| Long context, 24K-token prompts, unique | — | 2,584 / 3,038 | 93 C | 8, 3.1 |
| Sustained batch | 3,541 RPM | 2,686 / 3,031 | 90 C (stock 91) | 117, 64.7 (same) |
| Sustained, 4 concurrent streams | — | 2,697 / 2,935 | 92 C | 320, 177 aggregate |
The die runs a couple of degrees warmer on average than under stock, 75 C against 74 on the agent trace, 84 against 81 sustained, because the controller lets the card use its thermal mass instead of blowing it down early. The peaks, which are what protect the silicon, are the same. Memory stayed at 76 to 80 C throughout.
What batching does to heat
Concurrency changes the efficiency side without touching the noise side. With four streams the card still sits at 300 W and the blower at the same speed, but it delivers 2.7 times the tokens.
| Streams | Requests | Aggregate tok/s | Energy per token | Blower, mean / peak | Die peak |
|---|---|---|---|---|---|
| 1 | 117 | 65 | 4.5 J | 2,686 / 3,031 RPM | 90 C |
| 4 | 320 | 177 | 1.6 J | 2,697 / 2,935 RPM | 92 C |
For a given amount of work the box therefore spends 63% less time at full blow, and each token costs a third of the energy. Under a continuous multi-user load the steady noise is unchanged, and the forecast handles the concurrent case with the exact per-slot progress the scheduler publishes.
Long context, the honest trade
A 24K-token prompt is 30 s of prefill at 300 W from a cool die, about one thermal time constant. That is the one workload where the forecast and the physics collide: the engine now predicts the 30 s correctly, and the model knows prefill runs hotter than decode, so holding a hard 92 C through it costs 3,300 to 3,600 RPM during the prefill. The previous model ran 400 RPM quieter by silently letting the peak reach 93 or 94 C. Which one a box should do is a config line, and for most clients the second is the right answer.
It has to fail safe
A fan controller that can be wrong about the future needs a floor it cannot fall through. The curve's upper points and the firmware's 100 C throttle are always there; on top, the service forces 100% fan if the die sits at 97 C, resets the curve on every exit, and falls back to the hand-fitted physics model with an extra 5 C of margin if its learned model is missing. We injected six faults under a sustained 300 W load:
| Fault | Behaviour |
|---|---|
| Hint file corrupted for 20 s | read as no hint, floor faded one step, die 88 C |
| Hint stale for 40 s | reactive fallback, floor re-planned the moment the hint returned |
| Engine killed, restarted after 25 s | service kept running, faded the floor, re-planned seconds after the load resumed |
| Service killed under load | curve reset instantly, firmware took over at 3,549 RPM, restart re-applied a 61% floor within 20 s |
| Started with no model file | built-in physics model with margin |
| Reset command afterwards | idempotent, curve at firmware default |
The same service was then run on the production STHT1 board with the DeepSeek V4 Flash hybrid stack on the R9700 and the Strix Halo: fan interface present, zero GPU errors, the curve reset on exit.
Hardware and setup
| Box | Lucebox: AMD Radeon AI PRO R9700 32 GB + Ryzen AI MAX+ 395 (Strix Halo), Ubuntu, kernel 7.0 amdgpu, ROCm 7.2 |
|---|---|
| Fan interface | amdgpu.ppfeaturemask with the overdrive bit set exposes gpu_od/fan_ctrl: a 5-point hotspot curve (50 to 100%), acoustic targets, minimum duty |
| Model under load | Qwen3.8-27B UD-IQ4_XS with the DFlash2 drafter, 300 W pinned in both prefill and decode |
| Engine hint | GET /v1/lucebox/thermal, or --thermal-hint-file written atomically every second |
| Thermal model | ridge regression, 25 features, 3 horizons, fitted on 64 recorded runs (22,738 one-second rows); refit nightly on the box from its own logs |
| Chassis | a 120 mm fan on the card's shroud at 100%: worth 4 C on memory and nothing on the die; the blower remains the only thing that moves air through the fin stack |
Reproduce it
Everything lives in lucebox_cooling/fan_forecast: the fan service with its unit tests, the dataset builder and the model fit, the load traces, every recorded run, and the fault-injection script. make test, make dataset, make fit. Deploy the harness to a box, run a trace under the stock and lucefan policies, and the same report script prints the table above.
Bottom line
A fan does not need to be smarter; it needs to be told. The inference engine already knows the next minute of its own heat, and a 25-weight model turns that into the slowest fan speed the physics allows. On the R9700 that is silence for chat, a third less fan for agent work and a quarter less for batch work, at the same peak temperatures and the same tokens per second. Below that, only a bigger fan or a passive card goes.
All numbers measured by us on one Lucebox (R9700 + Strix Halo) on September 15 to 17, 2026, at 1 Hz from the card's own sensors, six minutes per run, identical request traces for every pair. Run logs, the dataset and the scripts are in the lucebox_cooling module. Fan noise is quoted in RPM; the fan-law conversion to dB is the standard n⁵ rule, not a microphone measurement.