
Toma la huella de la GPU exacta en la que estás ejecutando, incluso entre GPUs idénticas, a partir del orden en que llegan sus sumas atómicas. 99,1% de precisión, sobrevive a reinicios, no se puede falsificar, y una llamada predeterminada a torch.index_add_ la filtra.
Entrada del blog: Telling identical GPUs apart from their numerical output
Dos GPUs del mismo modelo calculan la misma suma en un orden ligeramente distinto, y esa diferencia basta para distinguirlas. Un pequeño kernel de CUDA identifica en cuál de 19 NVIDIA A40 idénticas se ejecutó con una precisión del 99,1%, usando únicamente los números que calcula. No lee ningún número de serie ni utiliza temporizadores.
torch.index_add_ también lo filtra. Leyendo únicamente la salida ordinaria de esa operación se identifica el chip con una precisión del 100% entre GPUs con el mismo driver, y del 78% entre drivers mixtos.Por qué funciona: cuando miles de bloques de hilos compiten por actualizar un mismo contador, el hardware decide quién llega primero. Diferencias mínimas de fabricación entre los núcleos de un chip sesgan ese orden de la misma manera cada vez.
El modo determinista de PyTorch elimina la filtración de index_add_. El repositorio contiene las sondas, todas las mediciones que respaldan las cifras siguientes y scripts que reproducen cada resultado en la CPU de un portátil.
| Resultado | Número | Reproducir |
|---|---|---|
Sonda activa, 19 A40 idénticas, canal P3 (solo salida numérica, sin %smid) | 99,1% LOO (111/112), azar 5,3%, separación 4,28x | analysis/pooled_analyze.py |
| Sonda activa, 8 A40 | P3 100% LOO, separación 4,09x, p de permutación = 0,0005 | analysis/analyze_puf.py |
| Confusión por driver: 4 A40 con el driver idéntico (580.159.04) | P3 100% (6,10x), P2 100% (5,43x) | analysis/analyze_puf.py |
| Reinicio: reinicio de contenedor, mismo die verificado por UUID (4 A40) | 100% de reidentificación (12/12), ambos canales | analysis/temporal_analyze.py |
| RTX 3090, 7 chips en 4 versiones de driver | P3 100% LOO (4,55x), P2 100% (8,12x), p = 0,0005 | analysis/cross_model_analyze.py |
| RTX 4090, 2 chips en el mismo host y driver | P3 6/6 (12,5x), P2 6/6 (17,8x) | analysis/cross_model_analyze.py |
| L40S, 2 chips | P3 4/4 (17,2x), P2 4/4 (13,8x) | analysis/cross_model_analyze.py |
torch.index_add_ pasivo, K=65536, 6 A40 | 100% mismo driver (9/9), 78% entre drivers | analysis/passive_analyze.py |
| Capacidad de firma pasiva, K=65536 | ~217 bits independientes (A40), ~239 (RTX 4090) | analysis/roc_capacity.py |
| Falsificación: el atacante recorre cada parámetro de lanzamiento que controla | mejor falsificación 0,616 de correlación frente a un mínimo genuino de 0,92 | analysis/forge_analyze.py |
| Límite: transformer vanilla fwd/bwd, GEMM denso, matmul TF32 | idéntico bit a bit entre chips, sin filtración | analysis/leak_analyze.py, analysis/channel_analyze.py |
Defensa: torch.use_deterministic_algorithms(True) | canal pasivo eliminado; coste de backward de 2,5-3,6x; sonda activa no afectada | analysis/defense_analyze.py |
LOO = clasificación leave-one-out por centroide más cercano de mediciones individuales frente a los centroides por chip. Separación = distancia entre chips / distancia dentro del chip.
La sonda (probes/probe_puf.py) es un pequeño kernel de cupy. Lanza 4096 bloques de 128 hilos. Cada hilo ejecuta la misma cadena fija de FMAs, y luego el hilo 0 de cada bloque toma un ticket de un contador global con atomicAdd(counter, 1). El ticket es la ranura de llegada de ese bloque. Nada de la entrada difiere entre chips; lo único que varía es qué bloques llegan primero.
Ese orden no es aleatorio. El distribuidor de trabajo asigna los índices de bloque a las SMs siguiendo un patrón casi fijo, y cada SM de un die determinado se ejecuta ligeramente más rápido o más lento que sus hermanas debido a la variación de fabricación. Promediado sobre 3000 lanzamientos, cada índice de bloque se estabiliza en un rango de llegada medio que refleja la velocidad de la SM en la que cae.
fixed kernel, fixed input
|
v
blocks race to atomicAdd --> arrival slot per block (hardware-decided)
|
v average over 3000 launches
mean arrival rank per block index (P3) / per physical SM via %smid (P2)
|
v subtract the cross-chip mean (removes the architecture)
per-chip residual --> nearest-centroid match against enrolled chips
Tres canales surgen de una sola ejecución:
%smid, sin temporizadores, sin contadores privilegiados. Este es el canal principal.%smid. El agrupamiento por SM físico promedia el ruido térmico, por lo que P2 se mantiene bajo carga: 100% de reidentificación (18/18) tras 60 s de estrés a plena carga.Restar la media entre chips elimina todo lo que los chips comparten (arquitectura, controlador, compilador) y deja el residuo por dado. Dos mediciones del mismo chip quedan cerca una de otra; las mediciones de chips diferentes no.
git clone https://github.com/divagr18/gpu-instance-fingerprint.git
cd gpu-instance-fingerprint
pip install -r requirements.txt # numpy + scipy, enough for every analysis
Ejecuta el análisis sobre los datos incluidos (CPU, no se necesita GPU):
python analysis/pooled_analyze.py # 19 A40s: 111/112 = 99.1%
python analysis/analyze_puf.py "a40_8chip/*.json" # 8 A40s: P3 100%
Ejecutar la sonda en una GPU:
pip install numpy cupy-cuda12x
python probes/probe_puf.py 3 0 > gpu_a_r0.json # 3 epochs, replicate 0 (about 2 s per epoch)
python probes/probe_puf.py 3 1 > gpu_a_r1.json
Recopila algunas réplicas de cada una de dos o más GPU del mismo modelo, luego:
python analysis/analyze_puf.py "path/to/*.json"
Cada registro JSON contiene el UUID de la GPU, el driver, la versión de cupy y el reloj y la temperatura por época junto a las mediciones. El análisis agrupa por nombre de modelo y etiqueta los chips por UUID.
Para ejecutar en máquinas remotas a través de SSH, scripts/run_remote.sh envía una sonda, la ejecuta en varios hosts en paralelo y recopila el JSON:
OUT_DIR=data/my_run REPS=4 scripts/run_remote.sh probe gpu1=root@HOST1:PORT gpu2=root@HOST2:PORT
docker/Dockerfile compila una imagen con CUDA 12.8, cupy y la sonda preinstalados (docker build -f docker/Dockerfile -t gpu-instance-fingerprint .).
Todos los comandos se ejecutan desde la raíz del repositorio en una CPU. Los scripts encuentran sus datos en data/ por sí solos.