build_views.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env python3
  2. """Build, inside the container, the two model folders the b12x vLLM needs -- symlinks + a few small JSON files, nothing
  3. copied, nothing written outside the container:
  4. <out>/model the served model: every snapshot file linked, the 33 PLE-table files (shipped in an unindexed
  5. `ple-table/` subfolder) linked at top level, and
  6. * model.safetensors.index.json = the shipped index + the 128 `ngram_embedding.shard_N.weight` keys
  7. and the table's `ngram_embedding.weight_scale`. ONLY those keys: two of the table files also carry
  8. 1,057 tensors of the original checkpoint (layer 1's original experts, shared expert and router
  9. gate). A loader that reads by index key never touches them; one that reads whole files would
  10. silently overwrite V8's healed layer 1 -- see the loader filter in patch_b12x.py.
  11. * config.json = shipped config + `text_config.ple_embedding_dtype` (b12x's PLE storage needs it;
  12. absent -> bf16 assumed) + the `Qwen3_8FlashNextForConditionalGeneration` architecture name this
  13. vLLM registers (it does not know `Qwen4ExpForConditionalGeneration`).
  14. <out>/draft-k10 the MTP draft: only the files the speculative head needs (its own tensors, the int4 lm_head, the
  15. embedding shard) + an index of just those keys + a config routing top-k 10 with the head's own
  16. shared-expert width (640; the model's is 1280). Slim on purpose: a draft folder that links every
  17. shard makes the draft load walk the whole model again (2026-09-11 and 2026-09-21 out-of-memory).
  18. usage: build_views.py <snapshot dir> <output root> prints one summary line; exit != 0 on any inconsistency
  19. """
  20. import glob, json, os, shutil, struct, sys
  21. snap, root = (os.path.abspath(a) for a in sys.argv[1:3])
  22. view, draft = os.path.join(root, "model"), os.path.join(root, "draft-k10")
  23. B12X_ARCH = "Qwen3_8FlashNextForConditionalGeneration"
  24. def fail(msg):
  25. sys.exit(f"build_views: {msg}")
  26. def header(path):
  27. with open(path, "rb") as fh:
  28. n = struct.unpack("<Q", fh.read(8))[0]
  29. h = json.loads(fh.read(n))
  30. h.pop("__metadata__", None)
  31. return h
  32. def fresh(d):
  33. if os.path.lexists(d):
  34. shutil.rmtree(d)
  35. os.makedirs(d)
  36. index = json.load(open(os.path.join(snap, "model.safetensors.index.json")))
  37. wm = dict(index["weight_map"])
  38. cfg = json.load(open(os.path.join(snap, "config.json")))
  39. text = cfg.get("text_config", cfg)
  40. parts = int(text.get("split_ngram_parts", 0)) or fail("config has no split_ngram_parts")
  41. # ---------------------------------------------------------------- served model view
  42. fresh(view)
  43. for name in sorted(os.listdir(snap)):
  44. if name in ("config.json", "model.safetensors.index.json", "ple-table"):
  45. continue
  46. os.symlink(os.path.join(snap, name), os.path.join(view, name))
  47. ple_files = sorted(glob.glob(os.path.join(snap, "ple-table", "*.safetensors")))
  48. ple_files or fail(f"no ple-table/*.safetensors in {snap}")
  49. shards, scale_key, table_dtype = set(), None, None
  50. for f in ple_files:
  51. base = os.path.basename(f)
  52. if os.path.lexists(os.path.join(view, base)):
  53. fail(f"table file name {base} collides with a model file")
  54. os.symlink(f, os.path.join(view, base))
  55. for key, meta in header(f).items():
  56. if ".ple.ple_embedding.ngram_embedding." not in key:
  57. continue # foreign tensors stay out of the index
  58. if key in wm:
  59. fail(f"{key} is already indexed -- unexpected checkpoint layout")
  60. wm[key] = base
  61. leaf = key.split(".ngram_embedding.", 1)[1]
  62. if leaf == "weight_scale":
  63. scale_key = key
  64. elif leaf.startswith("shard_") and leaf.endswith(".weight"):
  65. shards.add(int(leaf[len("shard_"):-len(".weight")]))
  66. table_dtype = table_dtype or meta["dtype"]
  67. if shards != set(range(parts)):
  68. fail(f"PLE table incomplete: {len(shards)} of {parts} shards found")
  69. if table_dtype != "F8_E4M3" or scale_key is None:
  70. fail(f"expected an fp8 table with a weight_scale, got {table_dtype} / scale {scale_key}")
  71. json.dump({"metadata": index.get("metadata", {}), "weight_map": wm},
  72. open(os.path.join(view, "model.safetensors.index.json"), "w"))
  73. text["ple_embedding_dtype"] = "float8_e4m3fn"
  74. archs = cfg.setdefault("architectures", [])
  75. if B12X_ARCH not in archs:
  76. archs.append(B12X_ARCH)
  77. json.dump(cfg, open(os.path.join(view, "config.json"), "w"), indent=2)
  78. # ---------------------------------------------------------------- slim MTP draft folder
  79. fresh(draft)
  80. keep = {k: v for k, v in wm.items()
  81. if k.startswith("mtp.") or k.startswith("lm_head.") or k.endswith("embed_tokens.weight")}
  82. if not any(k.startswith("mtp.") for k in keep):
  83. fail("no mtp.* tensors in the index -- this snapshot has no speculative head")
  84. for name in sorted(os.listdir(view)):
  85. if name.endswith(".safetensors") and name not in set(keep.values()):
  86. continue
  87. if name in ("config.json", "model.safetensors.index.json"):
  88. continue
  89. os.symlink(os.path.join(view, name), os.path.join(draft, name))
  90. json.dump({"metadata": {}, "weight_map": keep}, open(os.path.join(draft, "model.safetensors.index.json"), "w"))
  91. dcfg = json.loads(json.dumps(cfg))
  92. dtext = dcfg.get("text_config", dcfg)
  93. dtext["num_experts_per_tok"] = 10
  94. head = header(os.path.join(snap, "model_extra_tensors.safetensors"))
  95. widths = {v["shape"][0] for k, v in head.items() if k.endswith("mlp.shared_expert.gate_proj.weight")}
  96. if len(widths) == 1:
  97. dtext["shared_expert_intermediate_size"] = widths.pop()
  98. json.dump(dcfg, open(os.path.join(draft, "config.json"), "w"), indent=2)
  99. print(f"model view: {len(os.listdir(view))} entries, +{len(shards)} table shards +1 scale indexed "
  100. f"({len(ple_files)} table files) | draft: {len(keep)} keys in {len(set(keep.values()))} files, top-k 10, "
  101. f"shared expert {dtext.get('shared_expert_intermediate_size')}")