IlyasMoutawwakil HF Staff commited on
Commit
462abd8
·
verified ·
1 Parent(s): 0746321

Add tiny random checkpoint

Browse files
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ tags:
4
+ - tiny-random
5
+ - quantized
6
+ ---
7
+
8
+ # tiny-random-Llama-NVFP4
9
+
10
+ A tiny random model for testing, shrunk from [`nvidia/Llama-3.1-8B-Instruct-FP4`](https://huggingface.co/nvidia/Llama-3.1-8B-Instruct-FP4): the same
11
+ architecture, quantization config and checkpoint layout at test sizes. Its key patterns, dtypes and tensor ranks match
12
+ the real checkpoint's (`scripts/extract_layout.py`).
13
+
14
+ modelopt NVFP4 on every decoder linear (packed U8, E4M3 `weight_scale` per 16, F32 `weight_scale_2`, calibrated F32 `input_scale`) plus the FP8 KV cache's `k_scale` / `v_scale`. Quantized with modelopt's `NVFP4QTensor`.
15
+
16
+ `reference/` holds the same weights dequantized to bf16, under the unquantized model's keys: the reference to compare
17
+ logits against, so a test measures what the load path and kernels add, not the quantization itself.
18
+
19
+ The weights are random; the outputs mean nothing. `scripts/` rebuilds it from the real checkpoint's `config.json`.
config.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 1,
8
+ "dtype": "bfloat16",
9
+ "eos_token_id": [
10
+ 1
11
+ ],
12
+ "head_dim": 64,
13
+ "hidden_act": "silu",
14
+ "hidden_size": 256,
15
+ "initializer_range": 0.02,
16
+ "intermediate_size": 512,
17
+ "max_position_embeddings": 4096,
18
+ "mlp_bias": false,
19
+ "model_type": "llama",
20
+ "num_attention_heads": 4,
21
+ "num_hidden_layers": 2,
22
+ "num_key_value_heads": 2,
23
+ "pad_token_id": null,
24
+ "pretraining_tp": 1,
25
+ "rms_norm_eps": 1e-05,
26
+ "rope_parameters": {
27
+ "factor": 8.0,
28
+ "high_freq_factor": 4.0,
29
+ "low_freq_factor": 1.0,
30
+ "original_max_position_embeddings": 8192,
31
+ "rope_theta": 500000.0,
32
+ "rope_type": "llama3"
33
+ },
34
+ "tie_word_embeddings": false,
35
+ "transformers_version": "5.18.0.dev0",
36
+ "use_cache": true,
37
+ "vocab_size": 1024,
38
+ "quantization_config": {
39
+ "config_groups": {
40
+ "group_0": {
41
+ "input_activations": {
42
+ "dynamic": false,
43
+ "num_bits": 4,
44
+ "type": "float",
45
+ "group_size": 16
46
+ },
47
+ "weights": {
48
+ "dynamic": false,
49
+ "num_bits": 4,
50
+ "type": "float",
51
+ "group_size": 16
52
+ },
53
+ "targets": [
54
+ "Linear"
55
+ ]
56
+ }
57
+ },
58
+ "ignore": [
59
+ "lm_head"
60
+ ],
61
+ "quant_algo": "NVFP4",
62
+ "kv_cache_scheme": {
63
+ "dynamic": false,
64
+ "num_bits": 8,
65
+ "type": "float"
66
+ },
67
+ "producer": {
68
+ "name": "modelopt",
69
+ "version": "0.37.0.dev5+g76fb12d47.d20250905"
70
+ },
71
+ "quant_method": "modelopt"
72
+ }
73
+ }
generation_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": [
5
+ 1
6
+ ],
7
+ "output_attentions": false,
8
+ "output_hidden_states": false,
9
+ "transformers_version": "5.18.0.dev0",
10
+ "use_cache": true
11
+ }
hf_quant_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "producer": {
3
+ "name": "modelopt",
4
+ "version": "0.37.0.dev5+g76fb12d47.d20250905"
5
+ },
6
+ "quantization": {
7
+ "quant_algo": "NVFP4",
8
+ "kv_cache_quant_algo": "FP8",
9
+ "group_size": 16,
10
+ "exclude_modules": [
11
+ "lm_head"
12
+ ]
13
+ }
14
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8a96839c1a64d95f02e40b69ac4126a34f26832ea9c9836f9a4c5ee3f3e556a4
3
+ size 1721712
reference/config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 1,
8
+ "dtype": "bfloat16",
9
+ "eos_token_id": [
10
+ 1
11
+ ],
12
+ "head_dim": 64,
13
+ "hidden_act": "silu",
14
+ "hidden_size": 256,
15
+ "initializer_range": 0.02,
16
+ "intermediate_size": 512,
17
+ "max_position_embeddings": 4096,
18
+ "mlp_bias": false,
19
+ "model_type": "llama",
20
+ "num_attention_heads": 4,
21
+ "num_hidden_layers": 2,
22
+ "num_key_value_heads": 2,
23
+ "pad_token_id": null,
24
+ "pretraining_tp": 1,
25
+ "rms_norm_eps": 1e-05,
26
+ "rope_parameters": {
27
+ "factor": 8.0,
28
+ "high_freq_factor": 4.0,
29
+ "low_freq_factor": 1.0,
30
+ "original_max_position_embeddings": 8192,
31
+ "rope_theta": 500000.0,
32
+ "rope_type": "llama3"
33
+ },
34
+ "tie_word_embeddings": false,
35
+ "transformers_version": "5.18.0.dev0",
36
+ "use_cache": true,
37
+ "vocab_size": 1024
38
+ }
reference/generation_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": [
5
+ 1
6
+ ],
7
+ "output_attentions": false,
8
+ "output_hidden_states": false,
9
+ "transformers_version": "5.18.0.dev0",
10
+ "use_cache": true
11
+ }
reference/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d605548fe78a18c4d21b9b17c050dab60e3d8411a0b52ed270ac9b0daf70f973
3
+ size 3412672
scripts/build_dense_bf16.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A tiny random dense model in bf16, from a real checkpoint's config.json shrunk in its own fields:
2
+ Qwen3, Llama or Mistral3 (Ministral-3, with its vision tower)."""
3
+
4
+ import argparse
5
+ import json
6
+ import tempfile
7
+
8
+ import torch
9
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoModelForImageTextToText
10
+
11
+ parser = argparse.ArgumentParser()
12
+ parser.add_argument("--real-config", required=True)
13
+ parser.add_argument("--out", required=True)
14
+ args = parser.parse_args()
15
+
16
+ text_sizes = dict(
17
+ vocab_size=1024,
18
+ hidden_size=256,
19
+ intermediate_size=512,
20
+ num_hidden_layers=2,
21
+ num_attention_heads=4,
22
+ num_key_value_heads=2,
23
+ head_dim=64,
24
+ max_position_embeddings=4096,
25
+ )
26
+ real = json.load(open(args.real_config))
27
+ real.pop("quantization_config", None)
28
+ for key in ("architectures", "transformers_version", "torch_dtype", "dtype"):
29
+ real.pop(key, None)
30
+ if "text_config" in real: # a VLM: shrink the decoder and the vision tower in their own sections
31
+ text = {k: v for k, v in real["text_config"].items() if k not in ("architectures", "quantization_config")}
32
+ shrunk = dict(
33
+ real,
34
+ text_config=dict(text, **text_sizes),
35
+ vision_config=dict(
36
+ real["vision_config"], hidden_size=64, intermediate_size=128, num_hidden_layers=2, num_attention_heads=2,
37
+ head_dim=32, image_size=112,
38
+ ),
39
+ image_token_index=1000,
40
+ )
41
+ else:
42
+ shrunk = dict(real, **text_sizes)
43
+ for token in ("bos_token_id", "eos_token_id", "pad_token_id"):
44
+ for section in (shrunk, shrunk.get("text_config", {})):
45
+ if section.get(token) is not None:
46
+ section[token] = [1] if isinstance(section[token], list) else 1
47
+ with tempfile.TemporaryDirectory() as tmp: # parsed from disk, the way the real config.json is
48
+ json.dump(shrunk, open(f"{tmp}/config.json", "w"))
49
+ config = AutoConfig.from_pretrained(tmp)
50
+ model_cls = AutoModelForImageTextToText if "vision_config" in config.to_dict() else AutoModelForCausalLM
51
+ torch.manual_seed(0)
52
+ model = model_cls.from_config(config).to(torch.bfloat16)
53
+ model.save_pretrained(args.out)
54
+ print(type(model).__name__)
scripts/calibrate_experts.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Per-expert activation amax of a bf16 MoE, as a modelopt calibration run collects it: the routed
2
+ hidden rows each expert's gate/up sees, and the intermediate its down sees. Writes
3
+ {"<layer>.<expert>.gate_up": amax, "<layer>.<expert>.down": amax, "<linear name>": input amax, "<linear name>:out": output amax}."""
4
+
5
+ import argparse
6
+ import json
7
+ import re
8
+
9
+ import torch
10
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoModelForImageTextToText
11
+
12
+ parser = argparse.ArgumentParser()
13
+ parser.add_argument("--bf16", required=True)
14
+ parser.add_argument("--out", required=True)
15
+ args = parser.parse_args()
16
+
17
+ vlm = "vision_config" in AutoConfig.from_pretrained(args.bf16).to_dict()
18
+ model_cls = AutoModelForImageTextToText if vlm else AutoModelForCausalLM
19
+ model = model_cls.from_pretrained(args.bf16, dtype=torch.bfloat16, device_map="cuda").eval()
20
+ amax = {}
21
+
22
+
23
+ def hook(name):
24
+ layer = re.search(r"layers\.(\d+)\.", name).group(1)
25
+
26
+ def record(module, inputs, _output):
27
+ hidden, top_k_index = inputs[0], inputs[1]
28
+ for expert in range(module.num_experts):
29
+ rows = hidden[(top_k_index == expert).any(-1)]
30
+ if rows.numel() == 0:
31
+ continue
32
+ gate, up = (rows @ module.gate_up_proj[expert].T).chunk(2, dim=-1)
33
+ inter = module.act_fn(gate) * up
34
+ for key, value in ((f"{layer}.{expert}.gate_up", rows), (f"{layer}.{expert}.down", inter)):
35
+ amax[key] = max(amax.get(key, 0.0), value.abs().max().item())
36
+
37
+ return record
38
+
39
+
40
+ def linear_hook(name):
41
+ def record(module, inputs, output):
42
+ amax[name] = max(amax.get(name, 0.0), inputs[0].abs().max().item())
43
+ # the output too: a k/v projection's is what an FP8 KV cache's `k_scale`/`v_scale` cover
44
+ amax[f"{name}:out"] = max(amax.get(f"{name}:out", 0.0), output.abs().max().item())
45
+
46
+ return record
47
+
48
+
49
+ for name, module in model.named_modules():
50
+ if name.endswith("mlp.experts"):
51
+ module.register_forward_hook(hook(name))
52
+ elif isinstance(module, torch.nn.Linear): # dense linears, for exports that quantize them too
53
+ module.register_forward_hook(linear_hook(name))
54
+
55
+ torch.manual_seed(0)
56
+ with torch.no_grad():
57
+ for _ in range(8):
58
+ model(torch.randint(3, model.config.get_text_config().vocab_size, (4, 128), device="cuda"))
59
+ json.dump(amax, open(args.out, "w"), indent=1)
60
+ print(f"{len(amax)} amax values")
scripts/extract_layout.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The key layout of a safetensors checkpoint, read from shard headers only (no tensor data).
2
+
3
+ Keys are normalized by replacing every integer path component with `N`, so a layout is the set of
4
+ distinct patterns with their dtypes, the number of keys behind each, and the layer indices each appears
5
+ at. `compare(real, tiny)` is what the tiny-checkpoint builders assert against.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ import struct
11
+ import sys
12
+ from collections import defaultdict
13
+ from pathlib import Path
14
+
15
+
16
+ def read_header(path):
17
+ with open(path, "rb") as f:
18
+ (n,) = struct.unpack("<Q", f.read(8))
19
+ header = json.loads(f.read(n))
20
+ header.pop("__metadata__", None)
21
+ return header
22
+
23
+
24
+ def normalize(key):
25
+ return re.sub(r"(?<=\.)\d+(?=\.|$)", "N", key)
26
+
27
+
28
+ def headers(source):
29
+ """`{key: {"dtype", "shape"}}` of a local checkpoint directory, or of a hub repo read remotely."""
30
+ if Path(source).is_dir():
31
+ for shard in sorted(Path(source).glob("*.safetensors")):
32
+ yield from read_header(shard).items()
33
+ return
34
+ from huggingface_hub import HfApi
35
+
36
+ for shard in HfApi().get_safetensors_metadata(source).files_metadata.values():
37
+ for key, tensor in shard.tensors.items():
38
+ yield key, {"dtype": tensor.dtype, "shape": tensor.shape}
39
+
40
+
41
+ def layout(source):
42
+ patterns = defaultdict(lambda: {"dtypes": set(), "count": 0, "layers": set(), "ndims": set()})
43
+ for key, info in headers(source):
44
+ entry = patterns[normalize(key)]
45
+ entry["dtypes"].add(info["dtype"])
46
+ entry["ndims"].add(len(info["shape"]))
47
+ entry["count"] += 1
48
+ if m := re.search(r"layers\.(\d+)\.", key):
49
+ entry["layers"].add(int(m.group(1)))
50
+ return {
51
+ p: {"dtypes": sorted(e["dtypes"]), "count": e["count"], "layers": sorted(e["layers"]), "ndims": sorted(e["ndims"])}
52
+ for p, e in sorted(patterns.items())
53
+ }
54
+
55
+
56
+ if __name__ == "__main__":
57
+ out = layout(sys.argv[1])
58
+ json.dump(out, open(sys.argv[2], "w"), indent=1)
59
+ print(f"{len(out)} patterns")
scripts/quantize_dense_nvfp4.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A tiny bf16 Llama written as nvidia/Llama-3.1-8B-Instruct-FP4 ships it, with modelopt's own NVFP4
2
+ quantizer: every decoder linear NVFP4 (packed U8 weight, E4M3 `weight_scale` per 16, F32 `weight_scale_2`),
3
+ a calibrated F32 `input_scale` per linear, and the FP8 KV cache's `k_proj.k_scale` / `v_proj.v_scale`
4
+ (output amax / 448). The real `quantization_config` and `hf_quant_config.json`; also <out>/dequantized,
5
+ the logits reference. Run in the producer venv (nvidia-modelopt)."""
6
+
7
+ import argparse
8
+ import json
9
+ import re
10
+ import shutil
11
+ from pathlib import Path
12
+
13
+ import torch
14
+ from modelopt.torch.quantization.qtensor import NVFP4QTensor
15
+ from safetensors.torch import load_file, save_file
16
+
17
+ parser = argparse.ArgumentParser()
18
+ parser.add_argument("--bf16", required=True)
19
+ parser.add_argument("--real", required=True, help="directory with the real config.json + hf_quant_config.json")
20
+ parser.add_argument("--calibration", required=True)
21
+ parser.add_argument("--out", required=True)
22
+ args = parser.parse_args()
23
+
24
+ source = load_file(f"{args.bf16}/model.safetensors")
25
+ calibration = json.load(open(args.calibration))
26
+ linear = re.compile(r"model\.layers\.\d+\.(self_attn|mlp)\.\w+_proj\.weight")
27
+ quantized, dequantized = {}, dict(source)
28
+ for key, tensor in source.items():
29
+ if not linear.fullmatch(key):
30
+ quantized[key] = tensor
31
+ continue
32
+ module = key.removesuffix(".weight")
33
+ qtensor, block_scale, global_scale = NVFP4QTensor.quantize(tensor.float(), 16)
34
+ quantized[key] = qtensor._quantized_data
35
+ quantized[f"{module}.weight_scale"] = block_scale
36
+ quantized[f"{module}.weight_scale_2"] = global_scale.reshape(()).float()
37
+ quantized[f"{module}.input_scale"] = torch.tensor(calibration[module] / (6.0 * 448.0), dtype=torch.float32)
38
+ for proj in ("k", "v"):
39
+ if module.endswith(f".{proj}_proj"):
40
+ quantized[f"{module}.{proj}_scale"] = torch.tensor(calibration[f"{module}:out"] / 448.0, dtype=torch.float32)
41
+ dequantized[key] = NVFP4QTensor(tensor.shape, torch.float32, qtensor._quantized_data).dequantize(
42
+ dtype=torch.float32, scale=block_scale, double_scale=global_scale, block_sizes={-1: 16}
43
+ ).to(torch.bfloat16)
44
+
45
+ out = Path(args.out)
46
+ real = json.load(open(f"{args.real}/config.json"))
47
+ for name, tensors in (("nvfp4", quantized), ("dequantized", dequantized)):
48
+ (out / name).mkdir(parents=True, exist_ok=True)
49
+ save_file({k: v.contiguous() for k, v in tensors.items()}, out / name / "model.safetensors", metadata={"format": "pt"})
50
+ config = json.load(open(f"{args.bf16}/config.json"))
51
+ if name == "nvfp4":
52
+ config["quantization_config"] = real["quantization_config"]
53
+ shutil.copy(f"{args.real}/hf_quant_config.json", out / name / "hf_quant_config.json")
54
+ json.dump(config, open(out / name / "config.json", "w"), indent=2)
55
+ shutil.copy(f"{args.bf16}/generation_config.json", out / name / "generation_config.json")
56
+ print(f"wrote {len(quantized)} tensors")