
जिस क्रम में आपके GPU के atomic adds आते हैं, उससे यह पहचानें कि आप किस GPU पर चल रहे हैं—समान GPUs के बीच भी सटीक पहचान। 99.1% सटीकता, reboots के बाद भी बनी रहती है, इसे नकली नहीं बनाया जा सकता, और एक सामान्य torch.index_add_ कॉल इसे लीक कर देता है।
ब्लॉग पोस्ट: Telling identical GPUs apart from their numerical output
एक ही मॉडल के दो GPUs एक ही योग को थोड़े अलग क्रम में गणना करते हैं, और यही अंतर उन्हें अलग पहचानने के लिए पर्याप्त है। एक छोटा CUDA kernel यह पहचानता है कि यह 19 समान NVIDIA A40s में से किस पर चला, 99.1% सटीकता के साथ, केवल उन संख्याओं का उपयोग करते हुए जिनकी वह गणना करता है। यह कोई सीरियल नंबर नहीं पढ़ता और कोई टाइमर का उपयोग नहीं करता।
torch.index_add_ कॉल भी इसे लीक कर देता है। केवल उस ऑप के सामान्य आउटपुट को पढ़कर ही एक ही ड्राइवर वाले GPUs के बीच चिप को 100% सटीकता से पहचाना जा सकता है, और मिश्रित ड्राइवरों के बीच 78% सटीकता से।यह क्यों काम करता है: जब हज़ारों थ्रेड ब्लॉक एक काउंटर को अपडेट करने के लिए दौड़ते हैं, तो हार्डवेयर तय करता है कि कौन पहले पहुँचता है। एक चिप के कोरों के बीच सूक्ष्म निर्माण-संबंधी अंतर उस क्रम को हर बार एक ही तरह से पूर्वाग्रहित करते हैं।
PyTorch का deterministic मोड index_add_ लीक को हटा देता है। रेपो में प्रोब्स, नीचे दी गई संख्याओं के पीछे का हर माप, और ऐसी स्क्रिप्ट्स हैं जो लैपटॉप CPU पर प्रत्येक परिणाम को पुनः उत्पन्न करती हैं।
| परिणाम | संख्या | पुनः उत्पन्न करें |
|---|---|---|
सक्रिय प्रोब, 19 समान A40s, P3 चैनल (केवल संख्यात्मक आउटपुट, कोई %smid नहीं) | 99.1% LOO (111/112), संयोग 5.3%, पृथक्करण 4.28x | analysis/pooled_analyze.py |
| सक्रिय प्रोब, 8 A40s | P3 100% LOO, पृथक्करण 4.09x, क्रमचय p = 0.0005 | analysis/analyze_puf.py |
| ड्राइवर भ्रम: समान ड्राइवर (580.159.04) पर 4 A40s | P3 100% (6.10x), P2 100% (5.43x) | analysis/analyze_puf.py |
| रीबूट: कंटेनर रीस्टार्ट, UUID द्वारा सत्यापित समान डाई (4 A40s) | 100% पुनः-पहचान (12/12), दोनों चैनल | analysis/temporal_analyze.py |
| RTX 3090, 4 ड्राइवर संस्करणों में 7 चिप्स | P3 100% LOO (4.55x), P2 100% (8.12x), p = 0.0005 | analysis/cross_model_analyze.py |
| RTX 4090, समान होस्ट और ड्राइवर पर 2 चिप्स | P3 6/6 (12.5x), P2 6/6 (17.8x) | analysis/cross_model_analyze.py |
| L40S, 2 चिप्स | P3 4/4 (17.2x), P2 4/4 (13.8x) | analysis/cross_model_analyze.py |
निष्क्रिय torch.index_add_, K=65536, 6 A40s | 100% समान-ड्राइवर (9/9), 78% क्रॉस-ड्राइवर | analysis/passive_analyze.py |
| निष्क्रिय हस्ताक्षर क्षमता, K=65536 | ~217 स्वतंत्र बिट्स (A40), ~239 (RTX 4090) | analysis/roc_capacity.py |
| जालसाज़ी: हमलावर अपने नियंत्रण वाले हर लॉन्च नॉब को स्वीप करता है | सर्वोत्तम जाली 0.616 सहसंबंध बनाम 0.92 असली न्यूनतम | analysis/forge_analyze.py |
| सीमा: वैनिला ट्रांसफॉर्मर fwd/bwd, डेंस GEMM, TF32 matmul | चिप्स के बीच बिट-समान, कोई लीक नहीं | analysis/leak_analyze.py, analysis/channel_analyze.py |
रक्षा: torch.use_deterministic_algorithms(True) | निष्क्रिय चैनल समाप्त; 2.5-3.6x बैकवर्ड लागत; सक्रिय प्रोब अप्रभावित | analysis/defense_analyze.py |
LOO = प्रति-चिप सेंट्रॉइड्स के विरुद्ध एकल मापों का leave-one-out nearest-centroid वर्गीकरण। पृथक्करण = चिप्स के बीच की दूरी / चिप के भीतर की दूरी।
प्रोब (probes/probe_puf.py) एक छोटा cupy kernel है। यह 128 थ्रेड्स के 4096 ब्लॉक लॉन्च करता है। प्रत्येक थ्रेड FMAs की वही निश्चित श्रृंखला करता है, फिर प्रत्येक ब्लॉक का थ्रेड 0 atomicAdd(counter, 1) के साथ एक ग्लोबल काउंटर से एक टिकट लेता है। वह टिकट उस ब्लॉक का आगमन स्लॉट है। इनपुट में चिप्स के बीच कुछ भी भिन्न नहीं होता; केवल यह भिन्न होता है कि कौन से ब्लॉक पहले पहुँचते हैं।
वह क्रम यादृच्छिक नहीं है। कार्य वितरक ब्लॉक इंडेक्स को लगभग-निश्चित पैटर्न में SMs पर मैप करता है, और किसी दिए गए डाई पर प्रत्येक SM निर्माण-संबंधी भिन्नता के कारण अपने सहोदरों की तुलना में थोड़ा तेज़ या धीमा चलता है। 3000 लॉन्चों पर औसत करने पर, प्रत्येक ब्लॉक इंडेक्स एक औसत आगमन रैंक पर स्थिर हो जाता है जो उस SM की गति को दर्शाता है जिस पर वह पड़ता है।
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
एक ही रन से तीन चैनल निकलते हैं:
%smid नहीं, कोई टाइमर नहीं, कोई विशेषाधिकार प्राप्त काउंटर नहीं। यह प्रमुख चैनल है।%smid रजिस्टर के माध्यम से भौतिक SM द्वारा बिन किए गए। भौतिक SM द्वारा बिनिंग तापीय शोर को औसत कर देती है, इसलिए P2 लोड के अंतर्गत टिका रहता है: 60 सेकंड के पूर्ण-लोड तनाव के बाद 100% पुनः-पहचान (18/18)।चिप्स के बीच माध्य घटाने से वह सब हट जाता है जो चिप्स साझा करते हैं (आर्किटेक्चर, ड्राइवर, कंपाइलर) और प्रति-डाई अवशेष बचा रहता है। एक ही चिप के दो माप एक-दूसरे के करीब आते हैं; भिन्न चिप्स के माप नहीं आते।
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
शामिल किए गए डेटा पर विश्लेषण चलाएँ (CPU, GPU की आवश्यकता नहीं):
python analysis/pooled_analyze.py # 19 A40s: 111/112 = 99.1%
python analysis/analyze_puf.py "a40_8chip/*.json" # 8 A40s: P3 100%
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
Collect a few replicates from each of two or more GPUs of the same model, then:
python analysis/analyze_puf.py "path/to/*.json"
प्रत्येक JSON रिकॉर्ड GPU UUID, ड्राइवर, cupy संस्करण और माप के साथ-साथ प्रति-epoch क्लॉक और तापमान दर्ज करता है। विश्लेषण मॉडल नाम के आधार पर समूहीकृत करता है और UUID द्वारा चिप्स को लेबल करता है।
SSH के माध्यम से दूरस्थ मशीनों पर चलाने के लिए, scripts/run_remote.sh एक प्रोब भेजता है, उसे कई होस्ट्स पर समानांतर रूप से चलाता है और JSON एकत्र करता है:
OUT_DIR=data/my_run REPS=4 scripts/run_remote.sh probe gpu1=root@HOST1:PORT gpu2=root@HOST2:PORT
docker/Dockerfile CUDA 12.8, cupy और प्रीइंस्टॉल किए गए प्रोब के साथ एक इमेज बनाता है (docker build -f docker/Dockerfile -t gpu-instance-fingerprint .)।
सभी कमांड CPU पर रेपो रूट से चलाए जाते हैं। स्क्रिप्ट्स अपना डेटा स्वयं data/ में ढूँढ लेती हैं।