Dev.to · 10 min read

Auditar un modelo de 180B sin descargarlo: leer cabeceras safetensors por HTTP Range con Python

Auditar un modelo de 180B sin descargarlo: leer cabeceras safetensors por HTTP Range con Python

TL;DR Antes de reservar 8 GPU o de bajar 360 GB, puedes auditar un checkpoint de Hugging Face leyendo solo las cabeceras de sus archivos safetensors. Lo hice con Darwin-180B-RSI-R3 (un MoE de 512 expertos, FINAL-Bench/Darwin-180B-RSI-R3): 131 shards, 262 peticiones HTTP Range y 229.760 bytes leídos (224 KiB, unas 1,57 millones de veces menos que los 360 GB del modelo). Con eso comprobé que los 131 shards tienen el tamaño que declara su cabecera, que los 1.658 tensores del índice coinciden uno a uno con los de las cabeceras y que el total es 179.999.981.459 parámetros, el mismo número que publica la API de Hugging Face. Además salen tres datos de diseño útiles: el 67,1 % de los parámetros son expertos enrutados, el 28,8 % son tablas de embeddings (n-gram) que no se multiplican enteras en cada token, y la caché K/V a 262.144 tokens ocupa 6,0 GiB por secuencia porque solo 12 de las 48 capas usan atención completa. Abajo está el script completo y una calculadora de memoria. Aviso de entrada: este es un modelo de VIDRAFT y esta cuenta es de VIDRAFT. Todo lo que mido son archivos públicos del repositorio, y cualquiera puede repetir el análisis con el código de este artículo. ¿Por qué auditar un checkpoint antes de descargarlo? Un modelo de 180B en BF16 pesa 360,0 GB (335,3 GiB) repartidos en 131 archivos. Si te equivocas al planificar, lo descubres después de horas de descarga o, peor, cuando vllm serve se queda sin memoria. Con unos pocos kilobytes de metadatos respondes de antemano tres preguntas de ingeniería: ¿Está completo y es coherente? (tamaños, offsets, índice) ¿Dónde están los parámetros? (expertos, atención, embeddings) ¿Cabe en mi hardware, y con cuánto contexto? ¿Cómo se leen las cabeceras safetensors sin bajar los pesos? El formato safetensors es deliberadamente simple: los primeros 8 bytes son un entero u64 little-endian con el largo n de una cabecera JSON; después vienen n bytes de JSON con, por cada tensor, su dtype, su shape y su data_offsets (inicio y fin dentro de la zona de datos). Los pesos van a continuación. Como Hugging Face sirve los archivos con soporte de Range, basta con pedir bytes=0-7 y luego bytes=8-(8+n-1). Este es el script completo que ejecuté (solo biblioteca estándar de Python): import json, struct, urllib.request from concurrent.futures import ThreadPoolExecutor from math import prod REPO = "FINAL-Bench/Darwin-180B-RSI-R3" BASE = f"https://huggingface.co/{REPO}/resolve/main/" def get(archivo, a=None, b=None): req = urllib.request.Request(BASE + archivo, headers={"User-Agent": "auditor/1.0"}) if a is not None: req.add_header("Range", f"bytes={a}-{b}") return urllib.request.urlopen(req, timeout=60).read() def cabecera(shard): n = struct.unpack("16,d} {100*v/total:6.2f} %") print(f"{'TOTAL':20s} {total:>16,d}") La salida real, medida hoy sobre el repositorio público: Categoría Parámetros % del total Expertos enrutados 120.795.955.200 67,11 % Embeddings (tablas n-gram + embed_tokens) 51.835.944.960 28,80 % Capa MTP 2.607.150.848 1,45 % Atención lineal 2.086.510.464 1,16 % Otros (normas, routers, mezcla) 736.378.915 0,41 % lm_head 635.699.200 0,35 % Atención completa 617.358.336 0,34 % Visión 448.931.056 0,25 % Experto compartido 236.052.480 0,13 % Total 179.999.981.459 100 % La suma coincide al parámetro con el campo safetensors.total de la API de Hugging Face (179.999.981.459, de los cuales 179.999.981.424 son BF16 y 35 son enteros I64). Esa coincidencia es la primera señal de que las cabeceras y el resumen de la plataforma cuentan la misma historia. ¿Qué comprobaciones de integridad se pueden hacer solo con cabeceras? Cuatro, todas sin descargar un solo peso: Comprobación Resultado en R3 Tamaño del archivo = 8 + n + mayor data_offsets[1] 131 de 131 shards Huecos entre tensores consecutivos dentro de un shard 0 Nombres en model.safetensors.index.json frente a nombres en cabeceras 1.658 y 1.658, conjuntos idénticos Tensores cuyo shard en el índice difiere del shard real 0 La primera es la más útil para detectar descargas truncadas o subidas incompletas: si el archivo tiene menos bytes que los que anuncia su cabecera, el shard está cortado. Para obtener el tamaño remoto usa GET /api/models/?blobs=true, que devuelve size y lfs.sha256 por archivo. Las cabeceras no demuestran que el contenido sea el correcto, solo que es estructuralmente consistente. Para el contenido existe el hash. Como prueba, bajé el shard más pequeño (model-00001-of-00131.safetensors, 1.040.155.944 bytes), calculé su SHA-256 en streaming y lo comparé con el lfs.sha256 de la API: coincide (db4dbc41ff71...). Si vas a descargar los 131, haz esa misma comparación shard por shard; es una sola línea con hashlib. ¿Dónde están los 180B parámetros de un MoE de 512 expertos? Los números de arriba tienen una lectura directa. Cada capa tiene un tensor fusionado experts.gate_up_proj de forma [512, 1280, 2560] y un experts.down_proj de forma [512, 2560, 640]. Con 48 capas salen 120.795.955.200 parámetros, es decir, 2.516.582.400 por capa y 4.915.200 por experto (3 x 2560 x 640). El router elige 10 de los 512 expertos por token. El segundo bloque llama la atención: 51,8 mil millones de parámetros, el 28,8 %, no son capas de cómputo. Son 128 tablas ngram_embedding de forma [2.500.012, 160] (51.200.245.760 parámetros, el 28,4 % del total) más el embedding de tokens. Por su forma y su nombre son tablas de consulta por fila: un token lee unas pocas filas, no multiplica la tabla entera. Para quien planifica memoria esto importa: esos 103,7 GB en BF16 tienen que estar residentes, pero no consumen ancho de banda en cada paso de decodificación como lo haría una capa densa del mismo tamaño. ¿Cuántos parámetros trabajan realmente en cada token? La ficha del modelo indica 10 expertos activos por token, pero no publica una cifra de parámetros activos. Con los tamaños de tensor puedo estimarla (es mi cálculo, no una cifra oficial): expertos_activos = 120_795_955_200 * 10 / 512 # 2.359.296.000 con_computo = (expertos_activos + 617_358_336 # atención completa + 2_086_510_464 # atención lineal + 236_052_480 # experto compartido + 635_699_200 # lm_head + 736_378_915) # otros print(f"{con_computo:,.0f} ({100*con_computo/179_999_981_459:.2f} % del total)") # salida: 6,671,295,395 (3.71 % del total) Son unos 6,67 mil millones de parámetros con cómputo por token en la ruta de texto (sin la capa MTP ni la visión), un 3,7 % del total. Para un decodificador de una sola secuencia, el límite superior de velocidad lo da el ancho de banda de memoria dividido por los bytes que se leen por token: Precisión GB leídos por token Límite con 100 GB/s Límite con 400 GB/s BF16 13,3 7,5 tok/s 30,0 tok/s ~4,5 bits por peso 3,75 26,6 tok/s 106,6 tok/s Son cotas teóricas ilustrativas (ignoran la lectura de las tablas de embeddings, el solapamiento de expertos entre tokens, la activación y la sobrecarga del motor). La ficha del modelo reporta, para el GGUF de 4 bits de 111 GB (POCKET-Darwin-180B-GGUF), entre 18 y 21 tok/s solo con CPU: es una cifra de la ficha, que no medí aquí, y cae dentro de la cota de 26,6 tok/s que sale de esta cuenta con 100 GB/s. ¿Cuánta memoria de caché K/V necesita un contexto de 262.144 tokens? Aquí el diseño híbrido marca la diferencia. En config.json, layer_types alterna tres capas linear_attention con una full_attention (full_attention_interval = 4), así que solo 12 de las 48 capas guardan K/V que crece con el contexto. Con num_key_value_heads = 2, head_dim = 256 y BF16: capas_completas, kv_heads, head_dim, bytes_ = 12, 2, 256, 2 por_token = capas_completas * 2 * kv_heads * head_dim * bytes_ # K y V print(por_token, por_token * 262_144 / 2**30) # salida: 24576 6.0 (bytes por token, GiB por secuencia) Son 24 KiB por token y 6,0 GiB por secuencia de 262.144 tokens. Si las 48 capas usaran atención completa serían 24 GiB. Las 36 capas de atención lineal guardan un estado de tamaño fijo. Suponiendo el diseño habitual [cabezas de valor, dim clave, dim valor] en float32 (48 x 128 x 128 x 4 bytes = 3,1 MB por capa, más un estado de convolución de 61 KB), son unos 115 MB por secuencia, independientes de la longitud. Esa suposición del layout del estado es mía; el resultado cambia poco el total porque el término dominante es el K/V de las 12 capas completas. Esta cuenta no incluye el índice de atención dispersa (indexer_*) ni la capa MTP, así que trátala como el K/V principal, no como el total exacto de un motor concreto. ¿Cabe en mi GPU? Una calculadora de memoria Con los pesos (359.999.962.918 bytes según model.safetensors.index.json) y el coste por secuencia (6,56 GB entre K/V y estados) se arma una calculadora corta: def secuencias_completas(n_gpu, gb_por_gpu, uso=0.90, pesos_gb=360.0, seq_gb=6.558): libre = n_gpu * gb_por_gpu * uso - pesos_gb return max(0, int(libre // seq_gb)) for nombre, n, gb in [("8xH100", 8, 80), ("4xH200", 4, 141), ("4xB200", 4, 192), ("2xB200", 2, 192)]: print(nombre, secuencias_completas(n, gb)) Configuración Memoria utilizable (90 %) Tras cargar los pesos Secuencias de 262k 8 x H100 80 GB 576,0 GB 216,0 GB 32 4 x H200 141 GB 507,6 GB 147,6 GB 22 4 x B200 192 GB 691,2 GB 331,2 GB 50 2 x B200 192 GB 345,6 GB no caben los pesos en BF16 0 Esto es una estimación de planificación: descuenta pesos, K/V y estados, y deja el 10 % restante para activaciones, grafos CUDA y fragmentación. La receta de la ficha usa --tensor-parallel-size 8 --enable-expert-parallel, que corresponde a la primera fila. Para menos memoria, la ficha enlaza la versión GGUF de 4 bits (111 GB) en lugar de BF16. Alcance de las cifras Todo el análisis sale de metadatos públicos y de un hash comprobado, medido el 2026-11-09. No ejecuté inferencia, así que no hay aquí ningún dato de calidad ni de velocidad medida; las velocidades son cotas teóricas y la cifra de la ficha se atribuye a la ficha. La clasificación de tensores por nombre es una heurística mía (los nombres siguen la convención del repositorio) y la cuadratura exacta con el total de la API la respalda. Lo que sí puedes hacer es repetir el script y obtener los mismos números. Preguntas frecuentes ¿Puedo leer las cabeceras safetensors sin descargar el archivo completo? Sí. Pide bytes=0-7 para obtener el largo n de la cabecera y luego bytes=8-(8+n-1) para leer el JSON. En este modelo, las 131 cabeceras suman 229.760 bytes. ¿Qué diferencia hay entre parámetros totales y parámetros activos en un MoE? El total cuenta todos los expertos (aquí 512 por capa); el activo cuenta los que procesan cada token (aquí 10 por capa, más la atención y el experto compartido). Por la forma de los tensores, en R3 son unos 6,67 mil millones de parámetros con cómputo por token, frente a 180 mil millones almacenados. ¿Cómo detecto una descarga truncada? Compara 8 + n + max(data_offsets[1]) con el tamaño del archivo. Si no coinciden, el shard está incompleto. Después verifica el SHA-256 con el valor lfs.sha256 de la API. ¿Por qué la caché K/V es tan pequeña a 262k tokens? Porque solo 12 de las 48 capas son de atención completa; las otras 36 son de atención lineal con estado fijo. Resultado: 24 KiB por token y 6,0 GiB por secuencia, frente a 24 GiB si todas las capas fueran de atención completa. ¿Sirve este método para otros modelos de Hugging Face? Sí, para cualquier repositorio con safetensors. Solo cambian el nombre del repo y, si quieres clasificar parámetros, las reglas de tipo() según la arquitectura. Further reading Un MoE de 180B en un portátil de 32 GB: el cuello de botella es el SSD, no la CPU ni la GPU Cómo correr un modelo de 180B sin GPU: POCKET-Darwin-180B en GGUF con llama.cpp

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News