
동일한 GPU들 사이에서도 원자적 덧셈이 도착하는 순서로부터 실행 중인 정확한 GPU의 지문을 채취합니다. 99.1% 정확도로 재부팅 후에도 유지되며, 위조할 수 없고, 기본 torch.index_add_ 호출만으로도 이 정보가 유출됩니다.
블로그 글: 수치 출력으로 동일한 GPU 구별하기
같은 모델의 GPU 두 개가 동일한 합계를 아주 약간 다른 순서로 계산하며, 그 차이만으로 두 GPU를 구별할 수 있습니다. 작은 CUDA 커널이 19개의 동일한 NVIDIA A40 중 어느 칩에서 실행되었는지를 오직 자신이 계산한 숫자만으로 99.1% 정확도로 식별합니다. 일련번호를 읽지도 않고 타이머도 사용하지 않습니다.
torch.index_add_ 호출로도 유출됩니다. 해당 연산의 평범한 출력만 읽어도 동일한 드라이버의 GPU들 사이에서 100% 정확도로 칩을 식별하며, 드라이버가 혼합된 경우에는 78%입니다.작동 원리: 수천 개의 스레드 블록이 하나의 카운터를 두고 경쟁할 때, 하드웨어가 누가 먼저 도착하는지를 결정합니다. 칩의 코어들 사이의 미세한 제조 차이가 그 순서를 매번 같은 방향으로 편향시킵니다.
PyTorch의 결정론적 모드(deterministic mode)는 index_add_ 유출을 제거합니다. 저장소에는 프로브, 아래 수치 뒤에 있는 모든 측정값, 그리고 각 결과를 노트북 CPU에서 재현하는 스크립트가 포함되어 있습니다.
| 결과 | 수치 | 재현 |
|---|---|---|
활성 프로브, 19개의 동일한 A40, P3 채널 (수치 출력만 사용, %smid 없음) | 99.1% LOO (111/112), 우연 확률 5.3%, 분리도 4.28x | analysis/pooled_analyze.py |
| 활성 프로브, A40 8개 | P3 100% LOO, 분리도 4.09x, 순열 p = 0.0005 | analysis/analyze_puf.py |
| 드라이버 교란: 동일한 드라이버(580.159.04)의 A40 4개 | P3 100% (6.10x), P2 100% (5.43x) | analysis/analyze_puf.py |
| 재부팅: 컨테이너 재시작, UUID로 동일 다이 검증 (A40 4개) | 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, A40 6개 | 동일 드라이버 100% (9/9), 드라이버 간 78% | analysis/passive_analyze.py |
| 수동 시그니처 용량, K=65536 | 약 217개의 독립 비트 (A40), 약 239개 (RTX 4090) | analysis/roc_capacity.py |
| 위조: 공격자가 제어하는 모든 실행 노브를 훑음 | 최고 위조 0.616 상관관계 vs 정품 하한 0.92 | analysis/forge_analyze.py |
| 경계: 바닐라 트랜스포머 fwd/bwd, dense GEMM, TF32 matmul | 칩 간 비트 단위 동일, 유출 없음 | analysis/leak_analyze.py, analysis/channel_analyze.py |
방어: torch.use_deterministic_algorithms(True) | 수동 채널 제거됨; 역전파 비용 2.5-3.6배; 활성 프로브 영향 없음 | analysis/defense_analyze.py |
LOO = 단일 측정값을 칩별 중심점(centroid)에 대해 분류하는 leave-one-out 최근접 중심점 분류. 분리도 = 칩 간 거리 / 칩 내 거리.
프로브(probes/probe_puf.py)는 작은 cupy 커널입니다. 128개 스레드로 이루어진 4096개의 블록을 실행합니다. 모든 스레드가 동일한 고정 FMA 체인을 수행한 뒤, 각 블록의 스레드 0이 atomicAdd(counter, 1)로 전역 카운터에서 티켓을 받습니다. 그 티켓이 해당 블록의 도착 슬롯입니다. 입력에서 칩 간에 다른 것은 아무것도 없으며, 유일하게 달라지는 것은 어떤 블록이 먼저 도착하는가입니다.
그 순서는 무작위가 아닙니다. 작업 분배기는 블록 인덱스를 거의 고정된 패턴으로 SM에 매핑하며, 주어진 다이의 각 SM은 제조 편차 때문에 형제 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
동일 모델의 GPU 두 개 이상에서 각각 몇 개의 반복 측정값을 수집한 다음:
python analysis/analyze_puf.py "path/to/*.json"
각 JSON 레코드는 측정값과 함께 GPU UUID, 드라이버, cupy 버전, 그리고 에포크별 클럭과 온도를 기록합니다. 분석은 모델 이름으로 그룹화하고 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/에서 데이터를 찾습니다.
| 결과 | 명령 | 데이터 |
|---|---|---|
| 8 A40s: P3 100% (4.09x, p=0.0005), P2 94% (4.63x), P1 28% (1.20x) | python analysis/analyze_puf.py "a40_8chip/*.json" | data/a40_8chip (8개 칩 x 4회 반복) |
| 동일 드라이버 4 A40s: P3 100% (6.10x), P2 100% (5.43x) | python analysis/analyze_puf.py "a40_8chip/puf8_[4678]_*.json" | data/a40_8chip |
| 통합: 12개 칩 98.6% (71/72); 19개 칩 99.1% (111/112), 4.28x, 칩 쌍의 97%가 칩 내 노이즈의 2배 이상으로 분리됨 | python analysis/pooled_analyze.py | a40_8chip, a40_stability (T1), a40_reboot (T1), a40_scale |
| 재부팅: P3 (마진 5.07x) 및 P2 (마진 5.03x)에 대해 100% 재식별 (12/12) | python analysis/temporal_analyze.py a40_reboot | data/a40_reboot (4개 칩) |
| 60초 스트레스: 기준선 LOO 96% (23/24); 재식별 P2 100% (18/18) | python analysis/temporal_analyze.py a40_stability | data/a40_stability (6개 칩) |
| RTX 3090 x7, RTX 4090, 3090 쌍, L40S 쌍 | python analysis/cross_model_analyze.py | data/rtx3090, data/rtx4090, data/cross_model |
| 수동 index_add (K=65536, K=4096), dense 대조군, scatter-overwrite | python analysis/passive_analyze.py | data/passive_* |
| ROC 및 비트 용량 | python analysis/roc_capacity.py | 모든 A40 프로브 데이터, passive_k4096, passive_k65536, rtx4090 |
| 위조: 최고 위조 0.616 대 정품 0.946 (하한 0.916), 임포스터 0.174 | python analysis/forge_analyze.py | data/forge (3개 A40s) |
| Transformer 경계 및 결정론적 모드 비용 | python analysis/leak_analyze.py | data/transformer_leak (3개 A40s) |
| TF32 matmul 및 DVFS 경계 | python analysis/channel_analyze.py | data/channels (3개 A40s) |
| 수동 채널에 대한 방어 | python analysis/defense_analyze.py | data/defense (2개 A40s) |
analysis/analyze_puf.py는 채널당 2000회 셔플 순열 검정을 실행하므로, 더 큰 세트는 몇 분이 걸립니다.
단일 측정의 모든 쌍이 채점되며(정품 = 동일 UUID, 임포스터 = 다른 UUID), 등록 평균화가 없으므로 이는 비관적인 단일 샷 동작점입니다. 자유도는 iris 방식이며, 임포스터 해밍 거리에 대해 DoF = p(1-p)/var(HD)입니다.
| 서명 | 쌍 | 점수 | AUC | EER | 용량 |
|---|---|---|---|---|---|
| 활성 P3, 19개 A40s | 644 정품 / 9367 임포스터 | 잔차 상관 | 0.974 | 9.3% (d' = 2.95) | |
| 활성 P3, 19개 A40s | 부호 해밍 | 0.847 | 26.9% | ~5 비트 | |
| 수동 K=4096, 8개 A40s | 부호 해밍 | 0.878 | 21.0% | ~97 비트 | |
| 수동 K=65536, 6개 A40s | 부호 해밍 | 0.952 | 11.1% | ~217 비트 | |
| 수동 K=65536, 3개 RTX 4090s | 부호 해밍 | 0.0% | ~239 비트 |
활성 채널은 잔차 상관을 통해 가장 잘 읽힙니다: 그 부호 패턴은 공간적으로 상관되어 있어, 정체성이 크기에 담겨 있습니다. 수동 부호 서명은 긴 비트 문자열처럼 동작하며, 그 용량은 K에 따라 증가합니다: K=65536에서 약 200+개의 독립 비트로, 매우 큰 플릿을 인덱싱하기에 충분합니다.
torch.index_add_활성 프로브는 커스텀 커널을 실행해야 합니다. 수동 변형은 표준 PyTorch 연산의 일반 출력만 읽습니다.
CUDA에서 torch.index_add_는 원자 연산으로 누적하므로, 각 출력 빈에 기여가 도착하는 순서는 하드웨어가 결정합니다. 일반적인 dense 영역(빈당 많은 기여)에서는 float32 합이 그 순서를 평균화합니다: 256개 빈에 2^20개의 기여가 있을 때, 모든 특징이 우연 수준으로 분류됩니다(6개 A40s에서 결합 17% 대 우연 17%). 해결책은 희소하고 높은 동적 범위의 영역입니다:
각 칩의 안정적인 원자적 순서는 그다음 안정적인 출력을 만들어낸다. 시그니처는 float64 참조값에 대한 평균 ULP 잔차의 빈별 부호이며, K비트 문자열이다.
pip install numpy torch
python probes/passive_index_add.py 0 65536 > passive_r0.json # replicate 0, K=65536