
Faça a impressão digital da GPU exata em que você está executando, mesmo entre GPUs idênticas, a partir da ordem em que suas adições atômicas chegam. 99,1% de precisão, sobrevive a reinicializações, não pode ser falsificada, e uma chamada padrão de torch.index_add_ a vaza.
Post no blog: Telling identical GPUs apart from their numerical output
Duas GPUs do mesmo modelo calculam a mesma soma em uma ordem ligeiramente diferente, e essa diferença é suficiente para distingui-las. Um pequeno kernel CUDA identifica em qual de 19 NVIDIA A40 idênticas ele foi executado com 99,1% de precisão, usando apenas os números que calcula. Ele não lê nenhum número de série e não usa temporizadores.
torch.index_add_ também o vaza. Ler apenas a saída comum dessa operação identifica o chip com 100% de precisão entre GPUs no mesmo driver, e 78% entre drivers mistos.Por que funciona: quando milhares de blocos de threads competem para atualizar um único contador, o hardware decide quem chega primeiro. Pequenas diferenças de fabricação entre os núcleos de um chip enviesam essa ordem da mesma forma todas as vezes.
O modo determinístico do PyTorch elimina o vazamento do index_add_. O repositório contém as sondas, todas as medições por trás dos números abaixo e scripts que reproduzem cada resultado em uma CPU de laptop.
| Resultado | Número | Reproduzir |
|---|---|---|
Sonda ativa, 19 A40 idênticas, canal P3 (apenas saída numérica, sem %smid) | 99,1% LOO (111/112), acaso 5,3%, separação 4,28x | analysis/pooled_analyze.py |
| Sonda ativa, 8 A40 | P3 100% LOO, separação 4,09x, permutação p = 0,0005 | analysis/analyze_puf.py |
| Confundidor de driver: 4 A40 no driver idêntico (580.159.04) | P3 100% (6,10x), P2 100% (5,43x) | analysis/analyze_puf.py |
| Reinicialização: reinício do contêiner, mesmo die verificado por UUID (4 A40) | 100% de reidentificação (12/12), ambos os canais | analysis/temporal_analyze.py |
| RTX 3090, 7 chips em 4 versões de driver | P3 100% LOO (4,55x), P2 100% (8,12x), p = 0,0005 | analysis/cross_model_analyze.py |
| RTX 4090, 2 chips no mesmo host e 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_ passivo, K=65536, 6 A40 | 100% mesmo driver (9/9), 78% entre drivers | analysis/passive_analyze.py |
| Capacidade de assinatura passiva, K=65536 | ~217 bits independentes (A40), ~239 (RTX 4090) | analysis/roc_capacity.py |
| Falsificação: atacante varre todos os parâmetros de lançamento que controla | melhor falsificação 0,616 de correlação vs piso genuíno de 0,92 | analysis/forge_analyze.py |
| Limite: transformer vanilla fwd/bwd, GEMM denso, matmul TF32 | bit a bit idêntico entre chips, sem vazamento | analysis/leak_analyze.py, analysis/channel_analyze.py |
Defesa: torch.use_deterministic_algorithms(True) | canal passivo eliminado; custo de backward de 2,5-3,6x; sonda ativa não afetada | analysis/defense_analyze.py |
LOO = classificação por centroide mais próximo leave-one-out de medições individuais contra centroides por chip. Separação = distância entre chips / distância dentro do chip.
A sonda (probes/probe_puf.py) é um pequeno kernel cupy. Ela lança 4096 blocos de 128 threads. Cada thread executa a mesma cadeia fixa de FMAs, então a thread 0 de cada bloco retira uma senha de um contador global com atomicAdd(counter, 1). A senha é o slot de chegada daquele bloco. Nada na entrada difere entre chips; a única coisa que varia é quais blocos chegam primeiro.
Essa ordem não é aleatória. O distribuidor de trabalho mapeia os índices de bloco para as SMs em um padrão quase fixo, e cada SM em um dado die executa um pouco mais rápido ou mais devagar que suas irmãs por causa da variação de fabricação. Em média sobre 3000 lançamentos, cada índice de bloco se estabiliza em um posto médio de chegada que reflete a velocidade da SM em que ele cai.
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
Três canais saem de uma única execução:
%smid, sem temporizadores, sem contadores privilegiados. Este é o canal principal.%smid. O agrupamento por SM físico elimina o ruído térmico por média, então P2 se mantém sob carga: 100% de reidentificação (18/18) após 60 s de estresse em carga total.Subtrair a média entre chips remove tudo o que os chips compartilham (arquitetura, driver, compilador) e deixa o resíduo por die. Duas medições do mesmo chip ficam próximas; medições de chips diferentes não.
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
Execute a análise nos dados incluídos (CPU, sem necessidade de GPU):
python analysis/pooled_analyze.py # 19 A40s: 111/112 = 99.1%
python analysis/analyze_puf.py "a40_8chip/*.json" # 8 A40s: P3 100%
Execute a sonda em uma 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
Colete algumas réplicas de cada uma de duas ou mais GPUs do mesmo modelo e, em seguida:
python analysis/analyze_puf.py "path/to/*.json"
Cada registro JSON contém o UUID da GPU, o driver, a versão do cupy e o clock e a temperatura por época, ao lado das medições. A análise agrupa por nome de modelo e rotula os chips pelo UUID.
Para executar em máquinas remotas via SSH, scripts/run_remote.sh envia um probe, executa-o em vários hosts em paralelo e coleta o JSON:
OUT_DIR=data/my_run REPS=4 scripts/run_remote.sh probe gpu1=root@HOST1:PORT gpu2=root@HOST2:PORT
docker/Dockerfile cria uma imagem com CUDA 12.8, cupy e a sonda pré-instalada (docker build -f docker/Dockerfile -t gpu-instance-fingerprint .).
Todos os comandos são executados a partir da raiz do repositório numa CPU. Os scripts encontram os seus dados em data/ por conta própria.