Actualización, 11 de octubre de 2026, 16:30: desde entonces he flasheado la BIOS 1.30. No arregla el desfase. tscsync sigue alineando los núcleos, pero en dos arranques con sincronización el kernel tiró el TSC igualmente, esta vez por su watchdog de clocksource y no por la comprobación de warp: BIOS 1.30: la sincronización aguanta, el TSC se va igual. Con tsc=nowatchdog añadido, el TSC aguantó, y cinco minutos de medida contra el reloj de tiempo real no encontraron ningún warp ni deriva: tsc=nowatchdog: cinco minutos, ningún warp. Un arranque no es una serie de arranques, así que lo llamo aguantando, no arreglado.

Cada arranque, el mismo aviso

Esto aparecía en el log del kernel de mi ThinkPad E595 en todos y cada uno de los arranques:

TSC synchronization [CPU#0 -> CPU#2]:
Measured 6601281288 cycles TSC warp between CPUs, turning off TSC clock.
tsc: Marking TSC unstable due to check_tsc_sync_source failed
clocksource: Switched to clocksource hpet

No se colgó nada. No salió nada en el escritorio. La máquina pasaba en silencio todo su reloj a un chip temporizador más lento desde el momento del arranque, y lo habría seguido haciendo para siempre si yo no hubiera estado leyendo el journal por otra cosa.

El Time Stamp Counter (TSC) es un contador dentro de cada núcleo de la CPU que avanza a un ritmo fijo. Leerlo cuesta una sola instrucción. Linux lo usa como su reloj principal, pero solo si todos los núcleos coinciden en la cuenta1. Si un núcleo va por detrás, el kernel no puede fiarse de él y recurre al High Precision Event Timer (HPET), un dispositivo temporizador aparte. El autor de tscsync midió una lectura del reloj en torno a 1,2 µs con el HPET, frente a decenas de nanosegundos con el TSC, y el kernel y el escritorio leen el reloj decenas de miles de veces por segundo1. Eso es latencia y batería, pagadas en cada arranque, para nada.

¿Y cuánto iba por detrás el núcleo? Más de tres segundos. En un portátil a 2,1 GHz, eso son 6 600 millones de ciclos.

Lo que entrega el firmware

El mensaje nombra la CPU 0 y la CPU 2, pero la culpa no es de la CPU 2. Medido por tscsync desde el gestor de arranque, y por el kernel en cuanto arranca, el cuadro es el mismo cada vez:

NúcleoDesfase respecto a la CPU 0Qué significa
CPU 0, el procesador de arranque (BSP)0la referencia, y el que no encaja
CPU 1 a 7, los demás núcleos (AP)entre +6 200 y +6 800 millones de cicloscoinciden entre sí con 32 ciclos de margen
Distancia a 2 096 MHzde 2,9 a 3,3 segundosel procesador de arranque va por detrás, en cada arranque

Siete núcleos coinciden. Uno llega tres segundos tarde. El firmware levanta el procesador de arranque, hace su trabajo en él y le pasa el control al gestor de arranque sin volver a poner nunca ese contador en línea con los demás.

El TSC del procesador de arranque antes y después de tscsyncAntes: tal como lo entrega el firmwareDespués: tscsync en modo syncCPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 unos 6 800 millones de ciclos (3,25 s) atrásCPU 1 a 7 coinciden con 32 ciclos de margenBSP adelantado, todos a menos de 45 ciclosLinux conserva la clocksource TSC
Izquierda: lo que entrega el firmware. Los siete procesadores de aplicación coinciden con 32 ciclos de margen; el procesador de arranque va unos 6 800 millones de ciclos por detrás de ellos. Derecha: después de que tscsync adelante el procesador de arranque en UEFI, todos los núcleos quedan a menos de 45 ciclos y la comprobación de warp del kernel pasa.

¿Por qué no puede Linux simplemente arreglarlo? En las CPU Intel hay un registro, IA32_TSC_ADJUST, que deja al kernel corregir el contador de un núcleo, y el código de sincronización del kernel lo usa cuando existe2. Las CPU AMD Zen no lo tienen. Sin él, nada en Linux puede reparar el desfase, y el intento dentro del kernel de escribir los contadores directamente (tsc=directsync) se rechazó upstream por poco fiable3.

Así que el kernel hace lo único honesto que le queda. Mide, encuentra que el tiempo va hacia atrás entre dos núcleos (lo que el kernel llama un warp), imprime el aviso y deja de usar el TSC2.

Cuarenta arranques, cuarenta warps

Un arranque es una anécdota. El journal de esta máquina se remonta al 30 de mayo de 2026, así que se lo pregunté.

ArranquesWarp, ciclosWarp a 2 096 MHz
Del 30 de mayo al 11 de octubre de 202640 de 40de 6 160 a 9 880 millonesde 2,94 a 4,72 s
Mediana6 570 millones3,14 s
Tras un apagado completo de unos 27 s16 600 millones3,15 s
Con tscsync en modo sync, BIOS 1.281ninguno registradoTSC conservado
BIOS 1.30, tscsync desactivado16 130 millones2,92 s
BIOS 1.30, tscsync en modo sync2ninguno registradoTSC tirado por el watchdog, ver más abajo
BIOS 1.30, modo sync y tsc=nowatchdog1ninguno registradoTSC conservado

Todos los arranques. Arranque en frío, reinicio, tras un apagado lo bastante largo como para esperar que cualquier resto se hubiera ido. La cantidad varía, lo que encaja con lo que encontró el propio autor de tscsync en un Lenovo Legion: allí, el warp crece casi exactamente con el tiempo que pasa la máquina en el firmware antes de que arranque el gestor de arranque3. Un arranque que se queda más rato en el logo sale más atrasado.

El arreglo tentador es tsc=reliable en la línea de comandos del kernel, que le dice a Linux que deje de comprobar. No lo hagas. Los núcleos están de verdad a tres segundos de distancia, y apagar la comprobación no cambia eso; solo deja que el kernel use un reloj que salta hacia atrás cuando una tarea pasa de un núcleo a otro. El README de tscsync dice lo mismo, y nombra los tirones de audio y vídeo que vienen después1.

¿Puedo preguntar por qué esto sigue saliendo a la venta?

No pregunto quién. Pregunto qué hubo en el proceso de Lenovo que dejó salir una BIOS que deja un núcleo tres segundos por detrás en cada arranque.

La respuesta está en las notas de diseño de tscsync, y no es halagadora. Fisher-Price OS (Windows) escribe él mismo los TSC al arrancar3. Un portátil validado solo con eso pasa la prueba, porque el sistema operativo recoge en silencio lo que deja el firmware. Linux mantiene su comprobación de seguridad y avisa de lo que le han dado, que es exactamente por lo que el fallo aparece ahí y en ningún otro sitio.

Lo incómodo es que, según parece, esto ya se ha arreglado antes. Hay usuarios que cuentan que Lenovo arregló la misma clase de fallo con una actualización de BIOS en el ThinkPad A485, el T14s Gen 1 AMD y el E4954. El E495 es el hermano de 14 pulgadas de esta máquina, y comparten una BIOS: el propio readme de Lenovo para el paquete lista los dos5.

Mi E595 estaba en la 1.28 y daba warp en cada arranque. Lenovo ha publicado desde entonces la 1.29 y la 1.30, y ninguna de las dos listas de cambios dice una palabra del TSC; la 1.30 arregla «system randomly freeze since BIOS V1.27» y algunos problemas de seguridad5. Así que instalé la 1.30 y volví a medir. El primer arranque con ella, con tscsync desactivado, registró un warp de 6 126 446 316 ciclos: 2,92 segundos, CPU#0 -> CPU#2, igual que siempre. La 1.30 no lo arregla. Los dos modelos comparten una BIOS, así que si el informe del E495 fuera correcto, esta máquina también estaría arreglada. El informe no da ninguna versión de BIOS ni enlaza ningún log4.

¿Va a volver Lenovo a un portátil de 2019 por un fallo que el sistema operativo con el que prueban les esconde? No.

tscsync, primero midiendo

tscsync es un pequeño programa UEFI que se ejecuta desde GRUB o systemd-boot justo antes de Linux. Le pide a cada núcleo su TSC a través de los propios servicios multiprocesador del firmware, calcula el desfase de cada núcleo respecto al procesador de arranque, adelanta los contadores rezagados hasta que coinciden, y luego comprueba el resultado con una prueba inspirada en la propia comprobación de warp del kernel1. Nunca escribe en la flash, en la NVRAM ni en los ajustes del firmware; los contadores que cambia se reinician al apagar1.

Eso es mucho que confiarle a un gestor de arranque, así que primero leí el código, en el commit 230eaa9, antes de compilarlo. La instalación la hice después como pide el proyecto:

PasoComandoQué salió
Compilarmakebinarios para GRUB y systemd-boot
Instalar, solo lecturasudo scripts/install.shmodo measure, nada escrito en los contadores
Apagar, arrancar, mirartscsync-statusCPU 0 a 2 964 ms por detrás de los 7 AP, y los AP coincidiendo con microsegundos de margen
Activarsudo scripts/set-mode.sh syncsurte efecto en el siguiente arranque

El modo measure es lo que merece la pena copiar en cualquier herramienta de este tipo. Te enseña el fallo en tu propio hardware antes de que se le permita cambiar nada, y un guardián contra cuelgues lo desactiva si un arranque nunca llega a Linux1.

El primer arranque en modo sync, directamente de tscsync-status:

survey (read-only):
  cpu  1 apic   1: offset 6815086011  rtt 126  BAD
  ...
  cpu  7 apic   7: offset 6815086010  rtt 378  BAD
  0 ok, 7 not ok
moving BSP forward by ~6815086010 cycles to match cpu 7:
  bsp vs cpu  7 apic   0: before 6815086001  after 3  rtt 357  iter 3  warps bsp/ap 0/0  OK
AP pass (corrects any AP still off):
  7 ok, 0 not ok
total 121 ms

Y el kernel, un segundo después:

clocksource: Switched to clocksource tsc-early
tsc: Refined TSC clocksource calibration: 2096.066 MHz
clocksource: Switched to clocksource tsc

Ninguna línea de warp. Ningún «unstable». /sys/devices/system/clocksource/clocksource0/current_clocksource dice tsc. 121 ms de arranque para tener el reloj que el hardware debía haber tenido desde el principio. Eso fue con la BIOS 1.28. La 1.30 no ha sido tan limpia.

Los 21 nanosegundos que Linux desautorizó

Hay una línea en ese informe que parece peor de lo que es, y merece la pena explicarla en vez de esconderla. La pasada final de verificación de tscsync marcó un núcleo:

cpu  7 apic   7: offset 45  rtt 357  warps bsp/ap 0/0  BAD
6 ok, 1 not ok
RESULT: some APs out of sync (Linux will fall back to HPET)

45 ciclos son unos 21 nanosegundos. tscsync apunta a una coincidencia de unos 10 ciclos1, así que según su propio objetivo la CPU 7 falló, y lo dijo. Luego arrancó Linux y no recurrió al HPET en absoluto.

Las dos comprobaciones hacen preguntas distintas:

Veredicto de desfase de tscsyncPrueba de warp del kernel
Pregunta¿a qué distancia están los contadores?¿va alguna vez el tiempo hacia atrás entre núcleos?
Métodoestimación a partir de un viaje de ida y vuelta cronometradolos núcleos leen por turnos y comparan con la última lectura2
Umbralunos 10 ciclosninguna lectura más baja que la anterior
CPU 7 en este arranque45 ciclos, BADningún warp, TSC conservado

El warps bsp/ap 0/0 de esa misma línea es tscsync ejecutando la prueba al estilo del kernel. No vio ninguno. El kernel tampoco.

El proyecto ya sabe que su veredicto puede ser más estricto que el del kernel. Un IdeaPad de su lista de hardware tenía cuatro núcleos marcados a entre 28 y 42 ciclos que el kernel aceptó, y desde la 2.1 un viaje de ida y vuelta lento se informa como UNSURE en lugar de BAD4. La regla del README para ese caso es la correcta: decide la propia comprobación del kernel, y si la clocksource es tsc, la ha pasado1. Un viaje de ida y vuelta de 357 ciclos no es lento en esta máquina, así que salió BAD. Una herramienta que enseña sus cálculos y luego deja que el kernel tenga la última palabra está hecha como debe.

BIOS 1.30: la sincronización aguanta, el TSC se va igual

Con la 1.30 volví a poner tscsync en modo sync y reinicié dos veces. Las dos veces hizo su trabajo. Adelantó el procesador de arranque unos 6 170 millones de ciclos, todos los núcleos quedaron a unas pocas decenas de ciclos, y el kernel no registró ningún warp. Las dos veces Linux acabó igualmente en el HPET unos segundos después:

tsc: Detected 2095.890 MHz processor
tsc: Refined TSC clocksource calibration: 2096.053 MHz
clocksource: Watchdog remote CPU 5 read timed out
clocksource: Marking clocksource tsc unstable due to frequency skew
clocksource: Watchdog                    hpet interval:        495136113ns
clocksource: Clocksource                  tsc interval:        496026671ns
tsc: Marking TSC unstable due to clocksource watchdog

Esta es otra comprobación. La prueba de warp se ejecuta a medida que se levanta cada núcleo y pregunta si el tiempo va alguna vez hacia atrás entre núcleos. El watchdog de clocksource sigue funcionando después del arranque. Más o menos cada medio segundo compara cuánto ha avanzado el TSC con cuánto ha avanzado el HPET, y si discrepan en más de un pequeño margen marca el TSC como inestable6. Una vez marcado, se queda así hasta el siguiente arranque.

Dos comprobaciones del kernel, y qué opción de arranque se salta cada unaFirmwareCPU 0 ~3 s atrástscsync, en UEFIadelanta la CPU 0Arranca Linuxsuben los núcleosPrueba de warpuna vez, al arrancarWatchdogcada 0,5 s vs HPETPor defectoambas activas: TSC tirado en 1.30✓ comprobada✓ comprobadatsc=nowatchdogeste arranque: TSC conservado✓ comprobada✗ omitidatsc=reliabledesaconsejado: oculta un warp real✗ omitida✗ omitida
Dos comprobaciones distintas pueden tirar el TSC. La comprobación de warp se ejecuta una vez, a medida que se levanta cada núcleo; tscsync es lo que hace que esta máquina la pase. El watchdog sigue comparando con el HPET cada medio segundo. tsc=nowatchdog se salta solo la segunda, mientras que tsc=reliable se salta las dos y escondería un warp real.
Arranque con la 1.30Comprobación de warpIntervalo HPETIntervalo TSCTSC frente a HPET
Primer arranque con syncpasada495 426 793 ns495 006 210 nsunas 850 ppm por debajo
Segundo arranque con syncpasada495 136 113 ns496 026 671 nsunas 1 800 ppm por encima

Entonces, ¿va el TSC al ritmo equivocado? No. Con la máquina ya arrancada, cronometré el TSC contra el reloj raw del kernel basado en el HPET durante 30 segundos, en ventanas de 5 segundos. Todas las ventanas dieron 2 096,062 MHz, a unas 4 partes por millón de la propia calibración del kernel. Un fallo de ritmo real no pasa de lento a rápido entre dos arranques para luego desaparecer. Algo perturba una ventana de medio segundo al principio del arranque, y la línea remote CPU 5 read timed out justo antes apunta a un núcleo que deja de responder por un momento. Que el firmware le robe tiempo a las CPU encajaría. No lo he demostrado.

Dos arranques es toda la evidencia que hay. Todavía no puedo decir si es la 1.30, el azar o algo que desencadena la escritura de tscsync. El único arranque con sync en la 1.28 conservó el TSC y los dos en la 1.30 lo perdieron, y eso no basta para culpar a la BIOS.

Para este caso hay una opción del kernel más acotada que tsc=reliable. tsc=nowatchdog apaga el watchdog de clocksource para el TSC y deja en paz la comprobación de warp del arranque7. Si tscsync alguna vez no lograra sincronizar, el kernel seguiría detectando el warp y recurriría al HPET. Esa era la siguiente prueba. Los números, y algunos cambios sugeridos para los propios informes de tscsync, están en el gestor de incidencias del proyecto8.

tsc=nowatchdog: cinco minutos, ningún warp

Añadí tsc=nowatchdog y reinicié. tscsync adelantó el procesador de arranque 6 207 104 421 ciclos, el kernel no registró ni warp ni desviación de frecuencia, y current_clocksource decía tsc. El primer arranque con la 1.30 que lo conservó.

Eso solo demuestra que lo que tiraba el TSC era el watchdog. No demuestra que sea seguro funcionar con el TSC, y con el watchdog apagado ya no hay nada en el kernel vigilándolo. Así que lo vigilé yo.

Mi comprobación de ritmo anterior no servía aquí. Cronometraba el TSC contra CLOCK_MONOTONIC_RAW, lo que estaba bien mientras el kernel funcionaba con el HPET. Con el TSC, ese reloj se construye a partir del TSC, así que la prueba compararía el contador consigo mismo. El HPET tampoco se puede leer desde el espacio de usuario, porque el kernel de Fedora se compila sin CONFIG_HPET_MMAP. El reloj de tiempo real (RTC) sí. Funciona con su propio cristal, y el kernel despierta a un programa en cada tic de su contador de segundos9.

Así que escribí una pequeña comprobación, tsccheck. Se fija a la CPU 0 y ejecuta cuatro pruebas cada 15 segundos:

PruebaCómoQué aspecto tiene un fallo
Ritmociclos del TSC contados entre tics de segundo del RTClos MHz derivando en un sentido
Desfase3 000 viajes de ida y vuelta de la CPU 0 a cada núcleo, desfase tomado del más rápidoun desfase que crece
Warpsen cada viaje, ¿leyó algún núcleo un valor más bajo que otro ya visto?cualquier cosa por encima de cero
Clocksourceleer current_clocksourcecualquier cosa que no sea tsc

El viaje de ida y vuelta es el núcleo de todo. La CPU 0 lee su contador y lo pasa, el otro núcleo lee el suyo y lo devuelve, y la CPU 0 vuelve a leer. Si los núcleos coinciden, la lectura del medio tiene que caer entre las otras dos:

Un viaje de ida y vuelta entre la CPU 0 y otro núcleoCPU 0CPU 1 a 7, de una en unatiempot0t1t2punto mediodesfasedesfase = t1 − (t0 + t2) / 2del más rápido de 3 000 viajeswarp si t1 < t0 o t2 < t1el tiempo retrocede: cualquiera es fallo
Un viaje de ida y vuelta. La lectura del otro núcleo, t1, se compara con el punto medio de las dos lecturas de la CPU 0, y el más rápido de 3 000 viajes da el desfase. Cualquier lectura más baja que otra ya vista es un warp, sea cual sea el desfase.

El bucle que lo hace:

for (int i = 0; i < ITERS; i++) {
	uint64_t t0 = rdtsc_o();                 /* CPU 0 */
	atomic_store(&pp.t0, t0);
	atomic_store(&pp.seq, 2 * i + 1);        /* the other core reads t1 now */
	while (atomic_load(&pp.seq) != 2 * i + 2)
		;
	uint64_t t2 = rdtsc_o();                 /* CPU 0 again */
	uint64_t t1 = atomic_load(&pp.t1);
	if (t1 < t0 || t2 < t1)
		r.warps++;                           /* time went backwards */
	if (i > 100 && t2 - t0 < r.rtt) {
		r.rtt = t2 - t0;
		r.offset = (int64_t)(t1 - (t0 + (t2 - t0) / 2));
	}
}

El programa entero es C normal para Linux x86-64, sin nada atado a tscsync ni a este portátil. Necesita root para /dev/rtc0, y el tercer argumento es la cifra de tu propio kernel, sacada de journalctl -k -b | grep 'Refined TSC':

gcc -O2 -Wall -pthread -o tsccheck tsccheck.c
sudo ./tsccheck 15 20 2096.073
tsccheck.c, la comprobación entera en 161 líneas tsccheck.c · 4 kB

SHA-256 02eba1966a0097196acd5dfdcad9c4b315cafc1854c60f34c5b56080e6c38408.

Veinte muestras en cinco minutos, nada más arrancar:

MedidaResultado
Clocksourcetsc en las 20 muestras, ningún mensaje del kernel
Warps0 en unos 420 000 viajes de ida y vuelta
Peor desfase respecto a la CPU 085 ciclos, unos 41 ns, sin tendencia
Ritmo en toda la ejecución2 096,027 MHz, entre 2 096,022 y 2 096,031 desde el primer minuto
Ritmo por ventana de 15 segundosde 2 095,943 a 2 096,104 MHz
Ritmo del TSC por ventana de 15 segundos, y en toda la ejecución+20 ppm0 ppm-20 ppm-40 ppm-60 ppm0 s60 s120 s180 s240 s300 scifra del kernel, 2 096,073 MHz15 s: ventana -11,3 ppm, toda la ejecución -11,3 ppm30 s: ventana -61,1 ppm, toda la ejecución -36,2 ppm45 s: ventana -23,5 ppm, toda la ejecución -32,0 ppm60 s: ventana +15,0 ppm, toda la ejecución -20,2 ppm75 s: ventana -21,6 ppm, toda la ejecución -20,5 ppm90 s: ventana -21,8 ppm, toda la ejecución -20,7 ppm105 s: ventana -19,6 ppm, toda la ejecución -20,6 ppm120 s: ventana -50,5 ppm, toda la ejecución -24,3 ppm135 s: ventana +5,8 ppm, toda la ejecución -20,9 ppm150 s: ventana -20,3 ppm, toda la ejecución -20,9 ppm165 s: ventana -23,6 ppm, toda la ejecución -21,1 ppm180 s: ventana -13,7 ppm, toda la ejecución -20,5 ppm195 s: ventana -29,5 ppm, toda la ejecución -21,2 ppm210 s: ventana -22,3 ppm, toda la ejecución -21,3 ppm225 s: ventana -20,0 ppm, toda la ejecución -21,2 ppm240 s: ventana -24,7 ppm, toda la ejecución -21,4 ppm255 s: ventana -19,1 ppm, toda la ejecución -21,3 ppm270 s: ventana -22,2 ppm, toda la ejecución -21,3 ppm285 s: ventana -62,0 ppm, toda la ejecución -23,5 ppm300 s: ventana +9,2 ppm, toda la ejecución -21,9 ppmuna ventana de 15 s, 61 ppm por debajotoda la ejecución hasta ahoraventana de 15 stoda la ejecución hasta ahora
Ritmo frente al RTC, en partes por millón respecto a la cifra del kernel. Las ventanas sueltas saltan por parejas; la cifra de toda la ejecución se estabiliza en unas 21 ppm por debajo durante el primer minuto y se queda ahí.

Las ventanas parecen peores de lo que son. Un tic del RTC que llega tarde hace una ventana larga y la siguiente corta, así que los errores vienen por parejas que se anulan: 61 partes por millón (ppm) por debajo a los 30 segundos, 15 ppm por encima a los 60. Una deriva no se anula sola. La cifra de toda la ejecución apenas se movió.

La cifra de toda la ejecución se queda 22 ppm por debajo de los 2 096,073 MHz del kernel durante los cinco minutos completos. Esa es la diferencia entre dos cristales, y se mantuvo quieta. chrony, que gobierna el reloj contra servidores de hora de internet, lo tenía entre 16 y 19 ppm por debajo10. Mismo sentido, mismo tamaño.

Los desfases saltan entre los mismos pocos valores, 10, 21 y 33 ciclos a un lado y otro, sin que ningún núcleo se aleje. Eso es ruido de un viaje de ida y vuelta de unos 550 ciclos. La verificación de tscsync seguiría llamando BAD a algunos. El kernel no.

¿Aguantó el TSC? Sí, en todas las medidas que tenía. Cinco minutos tras un único arranque siguen sin ser una serie de arranques. Lo siguiente es la misma comprobación tras mucho tiempo encendida, y otra vez a lo largo de unos cuantos reinicios más.

El arreglo es cosa del firmware

tscsync no debería tener que existir. Su propio README lo dice: los fabricantes de firmware son bienvenidos a usar el enfoque y el código, «which is where this should be fixed»1. Lenovo tiene el código fuente. El autor de tscsync solo tenía lo que su portátil le quisiera contar, y 80 arranques de logs1.

Eso es lo que se queda. El arreglo necesitó a un desconocido de fuera de la empresa para medir, diagnosticar y rodear el problema, mientras la firma que vendió el hardware lo validaba contra el único sistema operativo que esconde el fallo. Una máquina que no puedes reparar porque su firmware es cerrado no es del todo tuya, está prestada con condiciones. Un firmware abierto con actualizaciones firmadas habría permitido arreglar esto una vez, como es debido, a cualquiera a quien le importara lo suficiente como para mirar.

Mientras tanto, buen trabajo del autor de tscsync. Primero el modo measure, un guardián contra cuelgues, los números a la vista y un veredicto honesto incluso cuando no coincide con el kernel. Así es como deberían hacerse las herramientas de bajo nivel.

Todo host KVM con el mismo firmware

En un portátil esto cuesta algo de latencia y algo de batería. En un hipervisor lo paga además cada invitado, y nada de lo que sigue es cosa de Proxmox. Todo vive en el código KVM del kernel Linux, así que es igual en cualquier host basado en KVM, sea lo que sea lo que corra encima: Proxmox VE, oVirt, OpenStack, o QEMU y libvirt a secas11.

Qué hace KVM cuando el host pierde su TSC

KVM solo les da a sus invitados un reloj estable cuando el propio host funciona con el TSC. En el código del kernel, el reloj maestro, una única referencia de tiempo compartida por todas las vCPU de una VM, solo se activa si host_tsc_clocksource es verdadero12. Si el host cae al HPET se desactiva, y cambian varias cosas a la vez:

Host con TSCHost con HPET, TSC marcado como inestable
Reloj maestro de KVM12activadodesactivado
Una vCPU se mueve a otro núcleo12nada que actualizaruna actualización de reloj forzada en todas las vCPU de esa VM, hasta 10 por segundo
El desfase de TSC del invitado12se deja como estáse reescribe en cada movimiento, y se pone en modo de recuperación
PVCLOCK_TSC_STABLE_BIT a los invitados12activadono activado
clock_gettime() en el invitado13leído en espacio de usuariouna llamada al sistema, decidido una vez cuando arranca el invitado
Lecturas del reloj del invitado entre vCPU14independientescada lectura comprobada contra un único valor global compartido

La última fila es la que escala mal. Sin el bit de estabilidad, el kernel del invitado no puede fiarse de que el reloj de cada vCPU coincida con los demás, así que cada lectura se compara con «last_value, a global accumulator» y se sustituye con un compare-and-exchange atómico si es más reciente14. Todas las vCPU del invitado escriben en ese único valor. Cuantas más vCPU leen el reloj, más hacen cola en él.

Nada de esto aparece como un error. La clocksource del invitado sigue diciendo kvm-clock, y nada en el invitado te va a decir por qué se ha vuelto más lento.

Cómo llega la clocksource del host a un invitado KVMHost con TSCClocksource del host: tscuna instrucción para leerloReloj maestro de KVM activadobit de estabilidad para los invitadosEl invitado lee en espacio de usuariovDSO, unos 28 nsHost con HPETClocksource del host: hpetun viaje al dispositivo temporizadorReloj maestro de KVM desactivadosin bit estable, actualiza en cada movimiento de vCPUEl invitado hace una llamada al sistemaun valor compartido, unos 190 nsEl mismo código del kernel en Proxmox VE, oVirt, OpenStack o QEMU y libvirt a secas
La clocksource del host decide lo que recibe cada invitado. Con el TSC, los invitados leen el reloj en espacio de usuario. Con el HPET, KVM deja su reloj maestro, cada movimiento de vCPU fuerza actualizaciones del reloj, y cada lectura del reloj en el invitado se convierte en una llamada al sistema comprobada contra un valor que comparten todas las vCPU.

Lo que cuesta, medido

Lo medí en este portátil en vez de dejarlo en teoría. La clocksource del host se puede cambiar en caliente, lo que apaga el reloj maestro de KVM igual que la caída en el arranque. Un invitado de usar y tirar arrancó el propio kernel del host con 4 vCPU y un initramfs diminuto cuyo único programa cronometra clock_gettime(). Dos ejecuciones de cada forma, primero el host, luego el invitado:

Nanosegundos por lectura del reloj, host con TSC frente a host con HPETHostescala de 0 a 2 400 nsHost, 1 hilo, host con TSC: 24 ns24 nsHost, 1 hilo, host con HPET: 1 542 ns1 542 ns1 hiloHost, los 8 núcleos, host con TSC: 29 ns29 nsHost, los 8 núcleos, host con HPET: 1 922 ns1 922 nslos 8 núcleosInvitado, 4 vCPUescala de 0 a 300 nsInvitado, 4 vCPU, 1 vCPU, host con TSC: 28 ns28 nsInvitado, 4 vCPU, 1 vCPU, host con HPET: 187 ns187 ns1 vCPUInvitado, 4 vCPU, las 4 vCPU, host con TSC: 31 ns31 nsInvitado, 4 vCPU, las 4 vCPU, host con HPET: 244 ns244 nslas 4 vCPUclocksource del host: tscclocksource del host: hpet
Nanosegundos por llamada a clock_gettime(), media de dos ejecuciones de cada forma. El host va de 61 a 73 veces más lento con el HPET; el invitado, unas 7 veces con una vCPU y 8 con las cuatro. Los paneles usan escalas distintas, porque las barras del HPET del host aplastarían las del invitado.

El invitado no paga directamente el precio del HPET, porque kvm-clock sigue leyendo el TSC por debajo. Lo que pierde es la lectura en espacio de usuario y la independencia entre vCPU, y solo con eso va siete veces más lento. Con las cuatro vCPU leyendo a la vez el coste sube otra vez, y eso es ese valor global compartido en acción. Un invitado con 32 o 64 vCPU hace más cola en él. Eso no lo he medido.

Pongámosle un número. Un invitado que lee el reloj un millón de veces por segundo en una vCPU gasta en ello el 2,8 % de esa vCPU en un host sano, y el 19 % en este. Las bases de datos, la máquina virtual de Java (JVM) y cualquier otra cosa que ponga marcas de tiempo a su trabajo leen mucho el reloj.

Esta ejecución además se queda corta. Cambiar la clocksource a mano apaga el reloj maestro pero no marca el TSC como inestable, así que las reescrituras de desfase de la tabla de arriba nunca se ejecutaron. La caída en el arranque de este E595 sí lo marca como inestable, y un host en ese estado paga las dos cosas.

clockbench.c, el benchmark, ejecutado en el host o como init de un invitado clockbench.c · 2 kB

Cómo comprobar un host, y los límites

Ya expliqué por qué los relojes de los invitados son frágiles de entrada en el artículo sobre ptp_kvm; esto es el mismo problema llegando desde abajo. Comprueba el host, no el invitado:

journalctl -k -b | grep -iE 'TSC warp|TSC unstable'
cat /sys/devices/system/clocksource/clocksource0/current_clocksource

tscsync se ejecuta desde GRUB o systemd-boot1, lo que cubre a la mayoría de los hosts KVM. Proxmox VE usa GRUB, o systemd-boot en una instalación UEFI con ZFS como raíz y Secure Boot desactivado15. Eso sí, sé honesto con los límites antes de acercarlo a un nodo de producción.

LímiteQué hacer al respecto
Solo AMD Zen e Hygon1los hosts Intel con TSC_ADJUST ya los arregla el kernel
Todas las máquinas de su lista de hardware son portátiles4trata un servidor como no probado e informa de él
El modo sync se niega en hardware fuera de su lista salvo que lo aceptes explícitamente1lee por qué antes de forzarlo
Un primer arranque con sync puede colgarseprimero el modo measure, consola fuera de banda abierta, un nodo cada vez
Una actualización de BIOS puede arreglar el fallovuelve a ponerlo en measure después y comprueba1
Una sincronización limpia puede perder igualmente el TSC por el watchdogcomprueba current_clocksource tras cada arranque; tsc=nowatchdog si la causa es el watchdog, y luego mídelo tú mismo

Un fallo de firmware que a un portátil le cuesta algo de batería a un hipervisor le cuesta un trozo de cada invitado que ejecuta, cada segundo que está encendido, y los dueños de esos invitados no tienen forma de verlo. Ese es el argumento para comprobar current_clocksource en cada host que tengas, lo haya fabricado quien lo haya fabricado.


  1. tscsync README — qué hace, el coste de leer el reloj con el HPET, por qué tsc=reliable no es un arreglo, el modelo de seguridad y la regla de UNSURE. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Linux kernel — arch/x86/kernel/tsc_sync.c — el manejo de TSC_ADJUST, la prueba de warp y el mensaje «Measured %Ld cycles TSC warp between CPUs, turning off TSC clock.». ↩︎ ↩︎ ↩︎

  3. tscsync — docs/DESIGN.md — AMD Zen no tiene IA32_TSC_ADJUST, tsc=directsync rechazado upstream, Windows escribe él mismo los TSC al arrancar, y el warp creciendo con el tiempo en el firmware en el Legion Pro 5. ↩︎ ↩︎ ↩︎

  4. tscsync — docs/HARDWARE.md — máquinas probadas, los núcleos del IdeaPad 5 aceptados a entre -28 y -42 ciclos, y los arreglos de BIOS de los que se informa en el A485, el E495 y el T14s Gen 1. ↩︎ ↩︎ ↩︎ ↩︎

  5. Lenovo — BIOS Update Utility readme, ThinkPad E495/E595, version 1.30 — los dos modelos en un mismo paquete, el historial de versiones hasta la 1.30 (14 de mayo de 2024) y su lista de cambios. ↩︎ ↩︎

  6. Linux kernel — kernel/time/clocksource.c — el watchdog de clocksource, su comparación de intervalos y el mensaje «Marking clocksource %s unstable due to frequency skew». ↩︎

  7. Linux kernel — kernel-parameters — tsc=reliable desactiva la verificación en tiempo de ejecución y las comprobaciones de estabilidad del arranque; tsc=nowatchdog desactiva solo el watchdog de clocksource. ↩︎

  8. tscsync issue #1 — Lenovo ThinkPad E595 — los informes de la 1.28 y la 1.30, el análisis del watchdog y los cambios sugeridos. ↩︎

  9. Linux kernel — Real Time Clock (RTC) Drivers for Linux — «The clock can also be set to generate an interrupt upon every clock update, thus generating a 1Hz signal», leído a través de /dev/rtc. ↩︎

  10. chrony — chronyc manual — tracking: «The ‘frequency’ is the rate by which the system’s clock would be wrong if chronyd was not correcting it.» ↩︎

  11. KVM — Management Tools — Proxmox VE, oVirt, OpenStack y las herramientas basadas en libvirt, todas gestionando KVM. ↩︎

  12. Linux kernel — arch/x86/kvm/x86.c — pvclock_update_vm_gtod_copy(): «If the host uses TSC clock, then passthrough TSC as stable to the guest», y PVCLOCK_TSC_STABLE_BIT activado solo con el reloj maestro. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  13. Linux kernel — arch/x86/kernel/kvmclock.c — el invitado activa el reloj en espacio de usuario VDSO_CLOCKMODE_PVCLOCK solo cuando PVCLOCK_TSC_STABLE_BIT está activado. ↩︎

  14. Linux kernel — arch/x86/kernel/pvclock.c — sin PVCLOCK_TSC_STABLE_BIT, cada lectura se comprueba contra «last_value, a global accumulator» con un compare-and-exchange atómico. ↩︎ ↩︎

  15. Proxmox VE — Host Bootloader — GRUB por defecto, systemd-boot para instalaciones UEFI con ZFS como raíz y Secure Boot desactivado. ↩︎