
Fingerprinte die exakte GPU, auf der du läufst, selbst unter identischen GPUs, anhand der Reihenfolge, in der ihre Atomic Adds eintreffen. 99,1 % Genauigkeit, übersteht Neustarts, lässt sich nicht fälschen, und ein standardmäßiger torch.index_add_-Aufruf gibt sie preis.
Blogbeitrag: Telling identical GPUs apart from their numerical output
Zwei GPUs desselben Modells berechnen dieselbe Summe in einer leicht unterschiedlichen Reihenfolge, und dieser Unterschied reicht aus, um sie voneinander zu unterscheiden. Ein kleiner CUDA-Kernel identifiziert, auf welcher von 19 identischen NVIDIA A40s er gelaufen ist, mit 99,1 % Genauigkeit – allein anhand der Zahlen, die er berechnet. Er liest keine Seriennummer und verwendet keine Timer.
torch.index_add_-Aufruf verrät ihn. Wenn man nichts außer der gewöhnlichen Ausgabe dieses Ops liest, lässt sich der Chip unter GPUs mit demselben Treiber mit 100 % Genauigkeit identifizieren und über gemischte Treiber hinweg mit 78 %.Warum es funktioniert: Wenn Tausende von Thread-Blöcken darum wetteifern, einen Zähler zu aktualisieren, entscheidet die Hardware, wer zuerst ankommt. Winzige fertigungsbedingte Unterschiede zwischen den Kernen eines Chips verzerren diese Reihenfolge jedes Mal auf dieselbe Weise.
Der deterministische Modus von PyTorch beseitigt das Leck über index_add_. Das Repository enthält die Probes, jede Messung hinter den untenstehenden Zahlen und Skripte, die jedes Ergebnis auf einer Laptop-CPU reproduzieren.
| Ergebnis | Zahl | Reproduzieren |
|---|---|---|
Aktive Probe, 19 identische A40s, P3-Kanal (nur numerische Ausgabe, kein %smid) | 99,1 % LOO (111/112), Zufall 5,3 %, Separation 4,28x | analysis/pooled_analyze.py |
| Aktive Probe, 8 A40s | P3 100 % LOO, Separation 4,09x, Permutation p = 0,0005 | analysis/analyze_puf.py |
| Treiber-Störfaktor: 4 A40s auf dem identischen Treiber (580.159.04) | P3 100 % (6,10x), P2 100 % (5,43x) | analysis/analyze_puf.py |
| Neustart: Container-Neustart, gleicher Die per UUID verifiziert (4 A40s) | 100 % Re-Identifikation (12/12), beide Kanäle | analysis/temporal_analyze.py |
| RTX 3090, 7 Chips über 4 Treiberversionen | P3 100 % LOO (4,55x), P2 100 % (8,12x), p = 0,0005 | analysis/cross_model_analyze.py |
| RTX 4090, 2 Chips auf demselben Host und Treiber | 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 |
Passiv torch.index_add_, K=65536, 6 A40s | 100 % gleicher Treiber (9/9), 78 % treiberübergreifend | analysis/passive_analyze.py |
| Kapazität der passiven Signatur, K=65536 | ~217 unabhängige Bits (A40), ~239 (RTX 4090) | analysis/roc_capacity.py |
| Fälschung: Angreifer durchläuft jeden von ihm kontrollierbaren Startparameter | beste Fälschung 0,616 Korrelation vs. 0,92 echte Untergrenze | analysis/forge_analyze.py |
| Grenze: Vanilla-Transformer fwd/bwd, dichtes GEMM, TF32-Matmul | bitidentisch über Chips hinweg, kein Leck | analysis/leak_analyze.py, analysis/channel_analyze.py |
Verteidigung: torch.use_deterministic_algorithms(True) | passiver Kanal eliminiert; 2,5-3,6x Backward-Kosten; aktive Probe unbeeinflusst | analysis/defense_analyze.py |
LOO = Leave-one-out-Nearest-Centroid-Klassifikation einzelner Messungen gegen die Zentroide pro Chip. Separation = Abstand zwischen Chips / Abstand innerhalb eines Chips.
Die Probe (probes/probe_puf.py) ist ein kleiner cupy-Kernel. Er startet 4096 Blöcke mit 128 Threads. Jeder Thread führt dieselbe feste Kette von FMAs aus, dann zieht Thread 0 jedes Blocks mit atomicAdd(counter, 1) ein Ticket von einem globalen Zähler. Das Ticket ist der Ankunftsplatz dieses Blocks. Am Input unterscheidet sich zwischen den Chips nichts; das Einzige, was variiert, ist, welche Blöcke zuerst ankommen.
Diese Reihenfolge ist nicht zufällig. Der Work-Distributor bildet Blockindizes in einem nahezu festen Muster auf SMs ab, und jede SM auf einem gegebenen Die läuft aufgrund von Fertigungsschwankungen etwas schneller oder langsamer als ihre Geschwister. Gemittelt über 3000 Starts pendelt sich jeder Blockindex auf einen mittleren Ankunftsrang ein, der die Geschwindigkeit der SM widerspiegelt, auf der er landet.
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
Drei Kanäle gehen aus einem Lauf hervor:
%smid, keine Timer, keine privilegierten Zähler. Dies ist der Hauptkanal.%smid-Register. Die Gruppierung nach physischer SM mittelt thermisches Rauschen heraus, sodass P2 unter Last standhält: 100 % Re-Identifikation (18/18) nach 60 s Volllast-Stresstest.Das Subtrahieren des Mittelwerts über alle Chips entfernt alles, was die Chips gemeinsam haben (Architektur, Treiber, Compiler), und hinterlässt das Residuum pro Die. Zwei Messungen desselben Chips liegen nahe beieinander; Messungen verschiedener Chips nicht.
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
Führen Sie die Analyse mit den enthaltenen Daten durch (CPU, keine GPU erforderlich):
python analysis/pooled_analyze.py # 19 A40s: 111/112 = 99.1%
python analysis/analyze_puf.py "a40_8chip/*.json" # 8 A40s: P3 100%
Die Probe auf einer GPU ausführen:
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
Sammeln Sie einige Replikate von jeder von zwei oder mehr GPUs desselben Modells und führen Sie dann Folgendes aus:
python analysis/analyze_puf.py "path/to/*.json"
Jeder JSON-Datensatz zeichnet die GPU-UUID, den Treiber, die cupy-Version sowie die Taktfrequenz und Temperatur pro Epoche neben den Messwerten auf. Die Analyse gruppiert nach Modellnamen und kennzeichnet Chips anhand der UUID.
Um auf entfernten Maschinen über SSH auszuführen, überträgt scripts/run_remote.sh eine Probe, führt sie parallel auf mehreren Hosts aus und sammelt das JSON ein:
OUT_DIR=data/my_run REPS=4 scripts/run_remote.sh probe gpu1=root@HOST1:PORT gpu2=root@HOST2:PORT
docker/Dockerfile baut ein Image mit CUDA 12.8, cupy und der vorinstallierten Probe (docker build -f docker/Dockerfile -t gpu-instance-fingerprint .).
Alle Befehle werden vom Repo-Stammverzeichnis aus auf einer CPU ausgeführt. Die Skripte finden ihre Daten selbstständig in data/.