* [PATCH] x86/tsc: Require a deviating refined calibration to be reproduced
@ 2026-09-22 9:17 Alexander Warth
2026-10-03 22:07 ` NvD Ss
0 siblings, 1 reply; 3+ messages in thread
From: Alexander Warth @ 2026-09-22 9:17 UTC (permalink / raw)
To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86
Cc: H . Peter Anvin, Paul E . McKenney, John Stultz, Stephen Boyd,
linux-kernel
tsc_refine_calibration_work() measures the TSC against HPET or the ACPI PM
timer over one second and installs the result when it is within 1% of the
early calibration. One sample is final. The comment above the function
acknowledges that the window can be disturbed, but the 1% comparison is
the only defense.
That stopped being sufficient when the clocksource watchdog started to
enforce skew limits of a few hundred ppm. The refined value is installed
along with CLOCK_SOURCE_CALIBRATED, and the watchdog holds two calibrated
clocksources to SHIFT_500PPM. A refined value which is off by more than
~500ppm, but by less than 1%, is accepted and gets the TSC marked
unstable in the next watchdog period. The early calibration which it
replaced was usable: quick_pit_calibrate() only succeeds when its error
is below 2^-11.
tsc_read_refs() does not catch this. It bounds the latency of each
reference readout, but not what happens between the two readouts which
delimit the window.
The single sample within 1% has been accepted since 08ec0c58fb8a ("x86:
Improve TSC calibration using a delayed workqueue"). At that time the
watchdog tolerated 62.5ms per 0.5s interval, i.e. 12.5%, so such a
sample was merely inaccurate and was kept. 2e27e793e280 ("clocksource:
Reduce clocksource-skew threshold") brought the tolerance for a pair of
fine-grained clocksources down to a few hundred ppm, and 763aacf86f1b
("clocksource: Rewrite watchdog code completely") made the calibrated
case explicit with SHIFT_500PPM.
This is reproducible on an AMD Ryzen 3 PRO 2200GE (Raven Ridge, ASUS
PRIME A320M-K, BIOS 6254). Raw values of two consecutive refinement
windows of the same boot:
ref=HPET ref_delta=15135866 tsc_cycles=3385560864 -> 3202.662 MHz
ref=HPET ref_delta=14659748 tsc_cycles=3276343968 -> 3200.006 MHz
Measured against NTP over several days the TSC runs at 3199.995 MHz. In
the first window TSC and HPET disagree by 881us, in the second one by a
few microseconds. Across boots that disagreement is 881..888us and does
not scale with the window length: 833ppm in a 1.057s window, 853ppm in a
1.041s window, 838ppm in a 1.054s window, and 1757ppm in a 0.503s
watchdog interval. A nearly constant accumulated error over windows of
different length is inconsistent with a frequency error and indicates a
one-off step of ~885us. A sampler which compares TSC and HPET every 20ms
shows a single 883us window and nothing in the following 15 seconds.
The step is in the TSC. A debug kernel which timestamps TSC, HPET and the
ACPI PM timer on every falling UIP edge of the RTC, which has an
oscillator of its own, reports for the second containing the event and
the one after it:
rtc: second 1 tsc=1000864480ns hpet=999992377ns pm=999992177ns
rtc: second 2 tsc= 999986137ns hpet=999993843ns pm=999994133ns
HPET and the ACPI PM timer advance by 999.992ms against the RTC in the
affected second, the same as in the other 39 seconds of the run, while
the TSC advances by 1000.864ms. The reference counters keep running and
the TSC jumps forward once. RTC, HPET and PMTMR all sit in the FCH, but
only the RTC is driven by a separate crystal.
The jump requires both a warm reset and the first entry into ACPI C2. It
is present in 9 of 9 warm reboots which are allowed to reach deep idle,
absent in 2 of 2 warm reboots with one CPU kept busy or with
processor.max_cstate=1, and absent in 2 of 2 boots from power off, S5 and
G3, with the same kernel and command line minutes apart. It coincides
with the first time all CPUs are idle. That points at the TSC restore on
the first C6 exit, i.e. at firmware. With hpet=disable the ACPI PM timer
produces the same result.
So the TSC on this machine takes a single ~880us step early in boot and
then runs at 0.1ppm for days. The change below does not make that TSC
trustworthy and does not try to. It prevents one disturbed measurement
from becoming a permanent frequency error. Where the step lands decides
what happens today: in a refinement window it corrupts tsc_khz for the
rest of the boot, in a watchdog interval it marks the TSC unstable. Only
the former is addressed here.
As a result every warm reboot of this machine ends up with HPET as
clocksource, which makes clock_gettime() take 1878ns instead of 23ns.
Keep the 1% sanity limit as is. Within that limit accept a refined
sample only when it matches a previous measurement within 2^-11:
- A sample which matches the early calibration confirms it and is
installed right away. When the first refined sample agrees with the
early calibration, no additional delay is introduced and the
installed value is the same as before.
- A sample which does not match is recorded and the measurement is
repeated. A later sample is installed when it matches either the
early calibration or any recorded sample. If two refined samples
match each other, then the early calibration is the value which is
discarded, i.e. the early calibration is not treated as
authoritative.
- After three samples without a match keep the early calibration and
do not set CLOCK_SOURCE_CALIBRATED. That is what already happens
when the 1% check fails. Three samples because that is the minimum
which still converges when both the early calibration and one
refined sample are off.
In the worst case this delays the registration of the final TSC
clocksource by two seconds. clocksource_tsc_early stays in use
meanwhile.
On the affected machine the first sample after a warm reboot is
discarded and the second one is installed. The TSC stays the
clocksource.
Assisted-by: Claude:claude-fable-5-1 sparse
Signed-off-by: Alexander Warth <alexander.warth@mailbox.org>
---
Notes for reviewers, below the --- as this is a single patch.
Tested
- v7.2.6 + this patch on the affected machine, two warm reboots: the
first refinement sample is discarded, the second is installed,
clocksource stays "tsc".
- Mainline v7.3-rc3 + this patch + "make x86_debug.config", one warm
reboot on the same machine. That kernel boots more slowly, the
refinement window closed before the first common idle, and the ~880us
step landed in a watchdog interval instead:
clocksource: Marking clocksource tsc unstable due to frequency skew
Watchdog hpet interval: 503192165ns
Clocksource tsc interval: 504072662ns
This patch does not help in that case, by construction. If you test it
on a slow or instrumented kernel you may well still see an unstable
TSC on such hardware.
- No new diagnostics in: x86_64 defconfig W=1, defconfig +
x86_debug.config W=1, allmodconfig W=1, i386 defconfig W=1, i386
without HPET_TIMER W=1, clang 18 W=1, sparse C=2. (x86_64 without
HPET_TIMER does not exist, CONFIG_HPET_TIMER is def_bool y there.)
- The state machine was additionally exercised in a userspace copy over
ten cases, including the 2^-11 boundary and A-B-A sequences.
- checkpatch.pl --strict is clean.
Not tested
- One machine, one model, one BIOS revision. I have no second affected
system.
- The give-up path after three non-matching samples was only exercised
in the userspace copy, never on hardware.
- No testing in virtual machines.
Open questions
1. Three samples means up to two extra seconds before clocksource_tsc is
registered on an affected machine, and no change at all on a machine
whose first sample matches the early calibration. Is that trade
acceptable, or should the retry be bounded differently?
2. The give-up path deliberately leaves CLOCK_SOURCE_CALIBRATED unset,
as the existing 1% path does, so the watchdog falls back to
SHIFT_4000PPM. Setting it would be stricter on machines like mine,
but it would change behaviour for every machine whose refinement is
rejected, including those with no usable reference timer at all, and
I have no data for those.
3. pr_info() on the give-up path: keep, downgrade to pr_debug(), or drop?
Deliberately not addressed
- The watchdog side. An earlier draft made the watchdog require a
frequency skew to be confirmed by a second interval. The RTC
measurement above rules that out: the watchdog is right when it sees
this step, so forgiving the first interval would hide a real defect.
Dropped.
- Two refinement windows which are disturbed by the same amount would
agree with each other and be installed. On the affected machine the
step happens once per boot, so this was not observed; in general it
is no worse than accepting a single sample, as the code does today.
- The root cause, which is firmware and not the kernel: the TSC is
apparently not restored correctly on the first C6 exit after a warm
reset. BIOS 6254 (01/2026) is the latest for this board. A vendor bug
report is the actual fix. This patch is about not letting a single
unconfirmed measurement become permanent, which is a generic problem.
- If the conclusion is that hardware which steps its TSC like this
should simply lose the TSC clocksource, then say so and I will drop
this. I would argue the other way: a one-off 0.9ms phase step during
boot, followed by 0.1ppm over days, is not worth trading a 23ns
clock_gettime() for a 1878ns one for the whole uptime, and it is
currently a matter of timing whether a given boot survives it at all.
Tool transparency (Documentation/process/generated-content.rst)
- The symptom was found by hand on my own machine: the TSC clocksource
was lost on every warm reboot. The analysis, the instrumentation
patches quoted above, this patch and this changelog were produced in a
long interactive session with the AI assistant named in the
Assisted-by tag - many prompts over several days, not a single
generated diff. I supplied the symptoms, logs and measurements, asked
for hypotheses and counter-arguments, and had the debug patches
written whose output is quoted above. sparse and checkpatch.pl were
run on the result.
- I have read every line, built it, booted it, and I am answering review
as its author.
arch/x86/kernel/tsc.c | 60 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 58 insertions(+), 2 deletions(-)
diff --git a/arch/x86/kernel/tsc.c b/arch/x86/kernel/tsc.c
index 723347e2c..b5b803950 100644
--- a/arch/x86/kernel/tsc.c
+++ b/arch/x86/kernel/tsc.c
@@ -341,6 +341,17 @@ __setup("tsc=", tsc_setup);
#define MAX_RETRIES 5
#define TSC_DEFAULT_THRESHOLD 0x20000
+/*
+ * Two frequency measurements are considered to describe the same clock
+ * when they agree within 2^-11 (~500ppm). That is the error bound of
+ * quick_pit_calibrate() and the skew which the clocksource watchdog
+ * tolerates between two CLOCK_SOURCE_CALIBRATED clocksources.
+ */
+#define TSC_FREQ_MATCH_SHIFT 11
+
+/* Maximum number of refined calibration samples */
+#define TSC_REFINE_MAX_SAMPLES 3
+
/*
* Read TSC and the reference counters. Take care of any disturbances
*/
@@ -1279,6 +1290,13 @@ int unsynchronized_tsc(void)
return 0;
}
+static bool tsc_freq_matches(unsigned long a, unsigned long b)
+{
+ unsigned long hi = max(a, b), lo = min(a, b);
+
+ return hi - lo <= (hi >> TSC_FREQ_MATCH_SHIFT);
+}
+
static void tsc_refine_calibration_work(struct work_struct *work);
static DECLARE_DELAYED_WORK(tsc_irqwork, tsc_refine_calibration_work);
/**
@@ -1292,15 +1310,19 @@ static DECLARE_DELAYED_WORK(tsc_irqwork, tsc_refine_calibration_work);
*
* If there are any calibration anomalies (too many SMIs, etc),
* or the refined calibration is off by 1% of the fast early
- * calibration, we throw out the new calibration and use the
- * early calibration.
+ * calibration, or it deviates from the early calibration and
+ * cannot be reproduced, we throw out the new calibration and use
+ * the early calibration.
*/
static void tsc_refine_calibration_work(struct work_struct *work)
{
+ static unsigned long seen[TSC_REFINE_MAX_SAMPLES];
static u64 tsc_start = ULLONG_MAX, ref_start;
+ static unsigned int nr_seen;
static int hpet;
u64 tsc_stop, ref_stop, delta;
unsigned long freq;
+ unsigned int i;
int cpu;
/* Don't bother refining TSC on unstable systems */
@@ -1364,6 +1386,40 @@ static void tsc_refine_calibration_work(struct work_struct *work)
if (abs(tsc_khz - freq) > tsc_khz/100)
goto out;
+ /*
+ * tsc_read_refs() bounds the latency of the individual readouts, but
+ * not what happens between them. If either counter is disturbed in the
+ * middle of the window, e.g. the TSC steps forward once, then all
+ * readouts are valid and the result is nevertheless off by hundreds of
+ * ppm.
+ *
+ * The refined value is installed along with CLOCK_SOURCE_CALIBRATED,
+ * which makes the clocksource watchdog apply its 500ppm limit. A
+ * disturbed sample beyond that limit gets the TSC marked unstable in
+ * the next watchdog period, which is worse than not refining at all.
+ *
+ * Therefore accept a sample only when it matches a previous
+ * measurement, which is either the early calibration or an earlier
+ * sample of the refinement. If no two measurements match, keep the
+ * early calibration and leave the clocksource uncalibrated.
+ */
+ if (!nr_seen)
+ seen[nr_seen++] = tsc_khz;
+
+ for (i = 0; i < nr_seen; i++) {
+ if (tsc_freq_matches(seen[i], freq))
+ break;
+ }
+
+ if (i == nr_seen) {
+ if (nr_seen == ARRAY_SIZE(seen)) {
+ pr_info("Refined TSC calibration not reproducible, using early calibration\n");
+ goto out;
+ }
+ seen[nr_seen++] = freq;
+ goto restart;
+ }
+
tsc_khz = freq;
pr_info("Refined TSC clocksource calibration: %lu.%03lu MHz\n",
(unsigned long)tsc_khz / 1000,
base-commit: fd73f4a6659897191fa0d40695fe370925dd3780
--
2.47.3
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] x86/tsc: Require a deviating refined calibration to be reproduced
@ 2026-10-03 22:07 ` NvD Ss
2026-10-04 11:55 ` Alexander Warth
0 siblings, 1 reply; 3+ messages in thread
From: NvD Ss @ 2026-10-03 22:07 UTC (permalink / raw)
To: alexander.warth; +Cc: x86, linux-kernel
Hi,
I appear to be seeing essentially the same TSC issue on another Raven
Ridge / ASUS A320 system.
Hardware:
- CPU: AMD Ryzen 5 2400G with Radeon Vega Graphics (Raven Ridge)
- Motherboard: ASUS PRIME A320M-C R2.0
- BIOS: 6254
- RAM: 16 GiB DDR4-2400
- GPU: NVIDIA GeForce RTX 4060
Kernel used for the tests below:
- 6.18.52-1-cachyos-lts and 7.2.8-2-cachyos
The CPU reports both constant_tsc and nonstop_tsc.
I originally discovered this because a timing-heavy Wine/Proton game
was performing extremely poorly. perf showed a surprising amount of
CPU time in read_hpet, which led me to check the clocksource.
With a normal warm boot, the kernel switches from TSC to HPET. One
representative boot with BIOS 6254 produced:
tsc: Fast TSC calibration using PIT
tsc: Detected 3593.185 MHz processor
clocksource: Switched to clocksource tsc-early
tsc: Refined TSC clocksource calibration: 3596.250 MHz
clocksource: Switched to clocksource tsc
clocksource: Marking clocksource tsc unstable due to frequency skew
clocksource: Watchdog hpet interval: 504269467ns
clocksource: Clocksource tsc interval: 503848443ns
tsc: Marking TSC unstable due to clocksource watchdog
TSC found unstable after boot, most likely due to broken BIOS. Use
'tsc=unstable'.
clocksource: Switched to clocksource hpet
The difference is about 421 us over ~504 ms, or roughly 835 ppm.
I also tested with "nohpet" to determine whether HPET itself was
responsible. The same problem occurred using the ACPI PM timer as the
watchdog/reference:
Command line: ... nohpet
tsc: Fast TSC calibration using PIT
tsc: Detected 3593.621 MHz processor
clocksource: Switched to clocksource tsc-early
clocksource: acpi_pm: ...
tsc: Refined TSC clocksource calibration: 3596.253 MHz
clocksource: Switched to clocksource tsc
clocksource: Marking clocksource tsc unstable due to frequency skew
clocksource: Watchdog acpi_pm interval: 496412812ns
clocksource: Clocksource tsc interval: 495998342ns
tsc: Marking TSC unstable due to clocksource watchdog
TSC found unstable after boot, most likely due to broken BIOS.
clocksource: Switched to clocksource acpi_pm
Again the discrepancy is about 414 us over ~496 ms, approximately the
same ~835 ppm.
The particularly interesting result is a full cold boot.
I removed all experimental clocksource parameters, ran with the normal
kernel command line, shut the machine completely down, waited for
power-off, and then powered it back on.
On that boot I got:
tsc: Fast TSC calibration using PIT
tsc: Detected 3593.621 MHz processor
clocksource: Switched to clocksource tsc-early
tsc: Refined TSC clocksource calibration: 3593.248 MHz
clocksource: Switched to clocksource tsc
TSC remained the active clocksource:
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
There was one:
clocksource: Watchdog remote CPU 6 read timed out
message during this cold boot, but TSC was not subsequently marked unstable.
This appears very similar to the warm-reset + first deep-idle/C-state
behavior described in your patch. The bad refinement on my machine is
about 3596.25 MHz, while a cold boot gives about 3593.25 MHz.
For an additional test, I temporarily booted a warm-booted system with:
tsc=reliable clocksource=tsc
This prevented the fallback and dramatically improved the affected
workload, but I have removed this workaround because it also prevents
the normal clocksource validation. systemd-timesyncd initially reached
the +500 ppm frequency correction limit during that test.
With a normal cold boot and no TSC-related kernel parameters, TSC
stays selected and NTP later reported:
Frequency: -9.956ppm
I do not consider the NTP measurement precise evidence by itself,
since it was still relatively early after boot, but it is consistent
with the cold-boot TSC behaving much better than the forced warm-boot
case.
The application-level effect is very large on this machine. Under
HPET, Deadlock running through Proton was approximately 15-25 FPS. A
perf recording showed, among other samples:
9.99% MainThrd [kernel.kallsyms] [k] read_hpet
8.58% swapper [kernel.kallsyms] [k] read_hpet
2.08% wineserver [kernel.kallsyms] [k] read_hpet
Keeping TSC as the clocksource increased performance dramatically.
After resolving some separate memory/indexing issues as well, the same
game now runs around 90-115 FPS with TSC active.
I also updated the motherboard BIOS specifically while investigating this.
The board was previously running:
ASUS PRIME A320M-C R2.0
BIOS 5007
2019-06-18
I updated it to:
BIOS 6254
The warm-reboot TSC behavior remained present after the update.
This seems noteworthy because your reported machine is a Ryzen 3 PRO
2200GE / PRIME A320M-K / BIOS 6254, while mine is a Ryzen 5 2400G /
PRIME A320M-C R2.0 / BIOS 6254. Both are Raven Ridge systems from the
same ASUS A320 motherboard generation.
P.S most of the wording for the above report has been produced and
written by ai after days of me trying to diagnose such a massive fps
difference in my game between windows and linux, logging and
troubleshooting was done by me with assistance from the ai. This is my
first time reporting like this but I would be happy to provide any
help I can.
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] x86/tsc: Require a deviating refined calibration to be reproduced
2026-10-03 22:07 ` NvD Ss
@ 2026-10-04 11:55 ` Alexander Warth
0 siblings, 0 replies; 3+ messages in thread
From: Alexander Warth @ 2026-10-04 11:55 UTC (permalink / raw)
To: NvD Ss
Cc: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
H. Peter Anvin, Paul E. McKenney, John Stultz, Stephen Boyd,
linux-kernel
On Sun, 4 Oct 2026 01:37:23 +0330, NvD Ss wrote:
> I appear to be seeing essentially the same TSC issue on another Raven
> Ridge / ASUS A320 system.
Thanks for the report. Your mail is not linked to the patch thread and
dropped part of the Cc list, so I have restored both.
> tsc: Detected 3593.185 MHz processor
> clocksource: Switched to clocksource tsc-early
> tsc: Refined TSC clocksource calibration: 3596.250 MHz
> clocksource: Switched to clocksource tsc
> clocksource: Marking clocksource tsc unstable due to frequency skew
> clocksource: Watchdog hpet interval: 504269467ns
> clocksource: Clocksource tsc interval: 503848443ns
This is the same failure, and it is the case which the patch addresses.
The watchdog sees the TSC 421us short over 504ms, i.e. 835ppm. With
tsc_khz at 3596.250 MHz that puts the real TSC frequency in this
interval at 3593.247 MHz. Your cold boot refines to 3593.248 MHz. So
nothing happens to the TSC in the watchdog interval. The step fell into
the refinement window, the disturbed sample was installed, and the
watchdog rejects the TSC for a frequency error which the kernel
introduced itself.
Your acpi_pm boot agrees: 414us over 496ms is 835ppm again, which
corrects 3596.253 MHz to 3593.250 MHz.
Unpatched 7.2.5 did the same here: 3202.720 MHz via HPET and 3202.698
MHz via acpi_pm instead of 3199.995 MHz, i.e. +852ppm and +845ppm, and
the TSC was lost both times.
If your refinement window is about as long as mine, 1.04 to 1.06s, then
835ppm is a step of about 870 to 885us. Here it is 881 to 888us, on a
TSC which runs at 3200 instead of 3593 MHz. That would be the same step
in time, not in cycles.
The other case exists as well. On a slow debug kernel the step landed
in a watchdog interval here. The refinement was correct and the TSC
interval was 880us longer than the HPET one.
> tsc=reliable clocksource=tsc
[...]
> the normal clocksource validation. systemd-timesyncd initially reached
> the +500 ppm frequency correction limit during that test.
That fits. tsc=reliable disables the watchdog, but the refinement still
runs. That boot kept 3596.25 MHz, so the clock was 835ppm slow, and NTP
cannot correct more than 500ppm.
With the patch, 3596.250 MHz is 853ppm off the early calibration, which
is beyond the 2^-11 (488ppm) limit. The sample is not installed and the
measurement is repeated. The next window should give about 3593.25 MHz,
which matches the early calibration and is installed. The acpi_pm boot
is 732ppm off and takes the same path.
It would help a lot if you could test this:
1. v7.2.8 plus the patch, a few warm reboots. arch/x86/kernel/tsc.c is
identical in v7.2.6, which I tested, and v7.2.8, so the patch
applies as is. It also applies to v6.18.52 with offsets, but I have
not built or booted that.
Expected: the "Refined TSC clocksource calibration" line shows up
about one second later than without the patch, with about 3593.2
MHz, and the clocksource stays tsc. If that is what you get, please
answer with a "Tested-by: Name <address>" line.
CachyOS kernels carry extra patches. A vanilla stable or mainline
build is the more convincing test, but either helps.
If the step lands in a watchdog interval instead, the TSC is still
marked unstable, with a TSC interval which is longer than the
watchdog one. The patch does not address that.
2. Without the patch: one warm reboot with processor.max_cstate=1 on
the kernel command line. That made the step go away here, as did
keeping one CPU busy, one boot each. If it does the same on your
machine, it is the same mechanism and not just the same symptom.
And three questions:
- How many warm reboots have you looked at, and did all of them lose
the TSC?
- The watchdog messages which you quoted are in the 7.2 format. Does
6.18.52 lose the TSC as well? The watchdog code is different there,
so the dmesg lines of such a boot would be useful.
- Did BIOS 5007 behave the same before the update? "remained present"
reads like it. That would mean that the problem has been there since
2019 firmware and is not specific to 6254.
Please leave the timestamps in the dmesg output, and please reply to
all in plain text, so that the list and the maintainers get it.
Thanks,
Alexander
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-04 11:55 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-22 9:17 [PATCH] x86/tsc: Require a deviating refined calibration to be reproduced Alexander Warth
2026-10-03 22:07 ` NvD Ss
2026-10-04 11:55 ` Alexander Warth
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®