Field note · diagnostics

Diagnosing thermal throttling during long inference runs

Tokens per second falling during a long run has at least four possible causes, and only one of them is heat.

Written against macOS 14 or later on Apple Silicon. The sustained run below was measured on a MacBook Pro, Apple M5 Pro, 48 GB, macOS 27.0 (build 26A5425a), Searoom 0.3.0. Figures for what the fair thermal state costs come from a five-model run on the same machine. One machine, one chassis.

A long generation starts fast and gets slower. The obvious explanation is heat, and it is often wrong. Context growth, Low Power Mode, memory pressure, and thermal throttling all produce the same symptom, and three of them are cheaper to rule out than the one everyone assumes.

Establish which one you have before changing anything.

The confound almost everyone hits first

Tokens per second falls as context grows, on a perfectly cool machine, with no throttling of any kind. Attention cost scales with the length of the sequence being attended over, so each new token in a long conversation is doing more work than the one before it. This is a property of the architecture rather than of your Mac: self-attention relates every position to every other position, described in Attention Is All You Need, the paper that introduced the Transformer.

This means the single most common observation, that generation slowed down over a long chat, is expected behaviour rather than a fault.

The test is straightforward. Start a fresh context on the same model and generate again. If the rate returns to its original value, context growth explains it and the machine is fine. If a fresh context is also slow, something is genuinely wrong and it is worth continuing.

Ask the system what it decided

macOS records thermal and performance warning levels, and reports them on demand:

pmset -g therm

On an unthrottled machine this reports that no thermal warning level, performance warning level, or CPU power status has been recorded, which is what the machine used here reported while idle. That is a clean bill of health.

If you want to watch levels change over time rather than read the current state, there is a second command, and it behaves differently:

pmset -g thermlog

This one streams continuously until you interrupt it. Start it before a long run, leave it in a terminal, and read it afterwards. It will not return on its own.

The programmatic equivalent, and the value Searoom displays, is the thermal state:

ProcessInfo.processInfo.thermalState
// .nominal | .fair | .serious | .critical

This is available on every Mac and needs no sensor access. It reports what the system decided rather than what an observer inferred from a temperature. Apple documents the property and its four states, and its guidance on responding to thermal state changes is written for applications that should shed work as the state rises.

Read the states as a gradient rather than a switch. nominal is unconstrained. fair is where measurable degradation begins. serious is a later stage of the same process, not its onset.

Temperature is the weakest signal available

Direct temperature is the number people want and the one least worth trusting for this diagnosis.

Availability is inconsistent. Temperature comes from best-effort SMC access, and key names vary by Mac model and macOS release. On the M5 Pro used here, none of the twelve candidate CPU and package keys returned a valid reading, so the only available temperature was the battery pack at 29.5 °C. A battery pack temperature says nothing about what a performance core is doing.

Even where a CPU sensor is readable, the threshold at which any given machine throttles is not published, varies by chassis, and depends on ambient conditions. A reading of 95 °C is not interpretable without knowing the limit it is being compared against.

The thermal state sidesteps all of that by reporting the decision instead of the input.

The four causes, and how to separate them

Slowdown during a long run: distinguishing causes
CauseTestSignature
Context growthStart a fresh contextRate returns to normal immediately
Low Power Modepmset -g | grep powermodeSlow from the very start, machine stays cool
Memory pressurevm_stat swap counters, twiceSustained non-zero swap traffic; other apps also slow
Thermal throttlingpmset -g thermWarning level recorded; degrades over minutes, recovers when idle

The shapes over time are distinct enough to separate on their own. Thermal throttling has a characteristic profile: full speed for the first minute or two, then a decline as heat accumulates, then a plateau at a reduced rate, then recovery after the machine sits idle for a while.

Memory pressure does not recover on idle, because the model is still resident. It also degrades everything else on the machine, not only inference, which thermal throttling does far less selectively.

Low Power Mode is flat. It is slow from the first token and stays exactly as slow, with no accumulation and no recovery. Apple documents the setting as one that reduces energy use to increase battery life and, on Macs with a fan, reduces fan noise. It publishes no figure for the effect on compute, so the size of the slowdown is unknown rather than assumed.

If it really is thermal

Ranked by effect on a fixed workload.

Improve airflow. Apple's guidance is to use a Mac laptop on a stable work surface that allows for good ventilation, and specifically not in bed, on a pillow, or under bedding. Soft material against the chassis is the case that guidance addresses, and it is the largest available improvement for the least effort. Apple does not publish where any given model draws its intake air, so be wary of advice that depends on a particular vent location.

Check ambient temperature. Thermal headroom is the gap between the chassis and the room, so a warm room removes headroom before the workload starts. Apple specifies an ambient operating range of 10 to 35 degrees Celsius for Mac laptops, which is the envelope the cooling system is designed against.

Reduce work per token. A more heavily quantised model or a smaller one produces less heat for the same output. This trades quality for sustained rate.

Accept a lower sustained rate. Every machine has a sustained throughput below its peak. A thin laptop is more constrained than a desktop enclosure, and no configuration change removes that difference.

What will not help: closing applications, if the thermal state is the binding constraint. Freeing memory addresses a memory problem. If the machine is throttling on heat, memory headroom changes nothing.

Measured: a sustained run that did not throttle

The method above is only worth anything if it can return a negative. Here it does.

Six minutes of continuous generation on a MacBook Pro, Apple M5 Pro, 48 GB, macOS 27.0 build 26A5425a, Searoom 0.3.0. The model was Devstral Small 2 24B Instruct at Q4_K_M, a dense 24B at a 4096 token context. Eleven requests were issued back to back, each with an identical 540 token prompt and each generating exactly 600 tokens, so every request did the same amount of work. That is what makes the numbers comparable.

Eleven identical requests, back to back
RequestTokens/secondFan 1 RPMTemperatureThermal state
117.532,31826.29 °Cnominal
218.803,63427.69 °Cnominal
318.414,74127.69 °Cnominal
518.124,95230.00 °Cnominal
717.954,95431.50 °Cnominal
817.894,95232.19 °Cfair
917.834,95432.89 °Cfair
1117.554,94633.59 °Cfair

Request one is the outlier and it is the warm-up: the first call pays for prompt processing on a cold cache. Peak throughput is request two at 18.80 tokens per second, and the run ends at 17.55. That is a decline of 6.6% over six minutes.

The table above lists four causes for a decline like that. This run controls for three of them.

Context growth is excluded by construction. Every request used a fresh context with an identical prompt and generated the same number of tokens. Nothing accumulated between requests, so the confound that explains most reported slowdowns cannot explain this one.

Low Power Mode was off, confirmed from isLowPowerModeEnabled in every sample.

Memory pressure was not a factor. Swap counters were effectively flat across the whole run: four pages of swap-in in six minutes, on a machine that had held 30 GB of weights earlier in the same session without stalling.

That leaves heat. The thermal state rose from nominal to fair at around the four minute mark and stayed there, never reaching serious, and throughput fell 6.6% from peak across the eleven requests.

That 6.6% is the shallow end of a real effect rather than evidence of its absence. A five-model run on this machine measured throughput inside fair falling 26.3% for one model and 32.9% for another, with neither reaching serious, while a 3B model that never left nominal stayed flat across 31 requests. The size of the loss tracks how hard the workload is; the direction is consistent.

So the reading for this run is that it reached fair late and paid a small price for it. Fans at roughly 5,000 RPM are still not by themselves evidence of throttling, because fan speed responds to heat rather than measuring lost performance. The thermal state is the signal, and fair is a warning rather than an all-clear.

Limits of this measurement

One machine, one chassis, one model, one macOS build, one ambient temperature, six minutes. A thinner chassis, a longer run, a denser model, or a warmer room could all produce a different answer, and a machine that does reach serious would show a far larger decline than 6.6%. No throttling curve is published here because this run did not produce one. The temperature column comes from the battery pack, the only sensor readable on this Mac, and is not a core temperature.

Definitions for thermal pressure and temperature source are in the metric reference. Why fan noise is a poor diagnostic is covered in why your Mac's fans spin up, and the memory-pressure alternative in swap I/O vs swap size.

Sources