Refactor JIT kernel and expert-pack directory layout (#36704)
This commit is contained in:
+2
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
|
||||
#include <sgl_kernel/utils.h> // For div_ceil, RuntimeCheck
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from sglang.kernels.jit.minicpm_sala.get_block_table import get_block_table
|
||||
|
||||
__all__ = ["get_block_table"]
|
||||
@@ -28,6 +28,7 @@ _GROUPS = (
|
||||
"layernorm",
|
||||
"mamba",
|
||||
"memory",
|
||||
"minicpm_sala",
|
||||
"mm",
|
||||
"moe",
|
||||
"quantization",
|
||||
|
||||
@@ -16,7 +16,7 @@ def _jit_add_constant_module(constant: int) -> Module:
|
||||
return load_jit(
|
||||
"add_constant",
|
||||
*args,
|
||||
cuda_files=["add_constant.cuh"],
|
||||
cuda_files=["elementwise/add_constant.cuh"],
|
||||
cuda_wrappers=[("add_constant", f"add_constant<{args}>")],
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""MiniCPM-SALA kernels."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.kernels.ops.minicpm_sala.get_block_table import get_block_table
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "get_block_table":
|
||||
from sglang.kernels.ops.minicpm_sala.get_block_table import get_block_table
|
||||
|
||||
globals()[name] = get_block_table
|
||||
return get_block_table
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return sorted(set(globals()) | set(__all__))
|
||||
|
||||
|
||||
__all__ = ["get_block_table"]
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
def _jit_ngram_embedding_module() -> Module:
|
||||
return load_jit(
|
||||
"ngram_embedding",
|
||||
cuda_files=["ngram_embedding.cuh"],
|
||||
cuda_files=["speculative/ngram_embedding.cuh"],
|
||||
cuda_wrappers=[
|
||||
("compute_n_gram_ids", "&NgramEmbeddingKernel::compute_n_gram_ids"),
|
||||
(
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
|
||||
from sglang.kernels.jit.minicpm_sala import get_block_table
|
||||
from sglang.kernels.ops.minicpm_sala import get_block_table
|
||||
from sglang.srt.layers.attention.minicpm.sparse_utils import (
|
||||
CompressionLevelMetadata,
|
||||
MiniCPMSparseMetadata,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Expert-pack build, inspection, and validation utilities."""
|
||||
+637
@@ -0,0 +1,637 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import gguf
|
||||
|
||||
try:
|
||||
from .format import (
|
||||
ENTRY_STRUCT,
|
||||
FLAG_IDENTITY_PAYLOAD,
|
||||
FLAG_TRIPLET_OBJECTS,
|
||||
HEADER_STRUCT,
|
||||
ROLE_NAMES,
|
||||
IndexEntry,
|
||||
PackHeader,
|
||||
align_up,
|
||||
inspect_pack,
|
||||
sha256_file,
|
||||
write_index,
|
||||
)
|
||||
except ImportError:
|
||||
from format import ( # type: ignore[no-redef]
|
||||
ENTRY_STRUCT,
|
||||
FLAG_IDENTITY_PAYLOAD,
|
||||
FLAG_TRIPLET_OBJECTS,
|
||||
HEADER_STRUCT,
|
||||
ROLE_NAMES,
|
||||
IndexEntry,
|
||||
PackHeader,
|
||||
align_up,
|
||||
inspect_pack,
|
||||
sha256_file,
|
||||
write_index,
|
||||
)
|
||||
|
||||
|
||||
FORMAT = "SGLANG-EXPERTPACK-v1"
|
||||
EXPERT_RE = re.compile(
|
||||
r"^blk\.(?P<layer>\d+)\.ffn_(?P<role>gate|up|down)_exps\.weight$"
|
||||
)
|
||||
COPY_CHUNK_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def write_json_atomic(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
with temporary.open("w", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(directory_fd)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
|
||||
|
||||
def hash_range(stream, offset: int, nbytes: int) -> str:
|
||||
digest = hashlib.sha256()
|
||||
stream.seek(offset)
|
||||
remaining = nbytes
|
||||
while remaining:
|
||||
chunk = stream.read(min(remaining, COPY_CHUNK_BYTES))
|
||||
if not chunk:
|
||||
raise EOFError(
|
||||
f"short read at source offset {offset}, {remaining} bytes remain"
|
||||
)
|
||||
digest.update(chunk)
|
||||
remaining -= len(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def copy_range(source_fd: int, output, offset: int, nbytes: int) -> str:
|
||||
digest = hashlib.sha256()
|
||||
copied = 0
|
||||
while copied < nbytes:
|
||||
chunk = os.pread(
|
||||
source_fd, min(COPY_CHUNK_BYTES, nbytes - copied), offset + copied
|
||||
)
|
||||
if not chunk:
|
||||
raise EOFError(f"short read at source offset {offset + copied}")
|
||||
output.write(chunk)
|
||||
digest.update(chunk)
|
||||
copied += len(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def load_inventory(
|
||||
path: Path, source: Path, expected_sha256: str
|
||||
) -> tuple[dict, list[dict], dict]:
|
||||
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
|
||||
headers = [row for row in rows if row.get("kind") == "source"]
|
||||
summaries = [row for row in rows if row.get("kind") == "summary"]
|
||||
tensors = [row for row in rows if row.get("kind") == "tensor"]
|
||||
if len(headers) != 1 or len(summaries) != 1:
|
||||
raise ValueError(
|
||||
"inventory must contain exactly one source and one summary record"
|
||||
)
|
||||
header = headers[0]
|
||||
if Path(header["path"]).resolve() != source.resolve():
|
||||
raise ValueError("inventory source path does not match --source")
|
||||
if header.get("source_sha256") != expected_sha256 or not header.get(
|
||||
"payload_hashes"
|
||||
):
|
||||
raise ValueError(
|
||||
"inventory is not a full-hash inventory for the requested source"
|
||||
)
|
||||
if int(header["size"]) != source.stat().st_size:
|
||||
raise ValueError("inventory source size does not match the source file")
|
||||
if len(tensors) != int(header["tensor_count"]):
|
||||
raise ValueError("inventory tensor count does not match its header")
|
||||
if any(not row.get("sha256") for row in tensors):
|
||||
raise ValueError("inventory contains a tensor without a payload hash")
|
||||
return header, tensors, summaries[0]
|
||||
|
||||
|
||||
def create_inventory(source: Path, source_sha256: str) -> tuple[dict, list[dict], dict]:
|
||||
reader = gguf.GGUFReader(source, "r")
|
||||
tensors = []
|
||||
inventory_digest = hashlib.sha256()
|
||||
with source.open("rb", buffering=0) as stream:
|
||||
for tensor in sorted(reader.tensors, key=lambda item: item.name):
|
||||
record = {
|
||||
"kind": "tensor",
|
||||
"name": tensor.name,
|
||||
"shape": [int(value) for value in tensor.shape.tolist()],
|
||||
"type": tensor.tensor_type.name,
|
||||
"type_id": int(tensor.tensor_type),
|
||||
"offset": int(tensor.data_offset),
|
||||
"nbytes": int(tensor.n_bytes),
|
||||
"sha256": hash_range(
|
||||
stream, int(tensor.data_offset), int(tensor.n_bytes)
|
||||
),
|
||||
}
|
||||
encoded = json.dumps(record, sort_keys=True).encode("utf-8") + b"\n"
|
||||
inventory_digest.update(encoded)
|
||||
tensors.append(record)
|
||||
header = {
|
||||
"kind": "source",
|
||||
"path": str(source.resolve()),
|
||||
"size": source.stat().st_size,
|
||||
"source_sha256": source_sha256,
|
||||
"gguf_data_offset": int(reader.data_offset),
|
||||
"gguf_alignment": int(reader.alignment),
|
||||
"tensor_count": len(tensors),
|
||||
"metadata_count": len(reader.fields),
|
||||
"payload_hashes": True,
|
||||
}
|
||||
summary = {
|
||||
"kind": "summary",
|
||||
"tensor_count": len(tensors),
|
||||
"inventory_sha256": inventory_digest.hexdigest(),
|
||||
}
|
||||
return header, tensors, summary
|
||||
|
||||
|
||||
def validate_inventory_against_reader(source: Path, tensors: list[dict]) -> None:
|
||||
reader = gguf.GGUFReader(source, "r")
|
||||
actual = {
|
||||
tensor.name: {
|
||||
"shape": [int(value) for value in tensor.shape.tolist()],
|
||||
"type": tensor.tensor_type.name,
|
||||
"type_id": int(tensor.tensor_type),
|
||||
"offset": int(tensor.data_offset),
|
||||
"nbytes": int(tensor.n_bytes),
|
||||
}
|
||||
for tensor in reader.tensors
|
||||
}
|
||||
recorded = {row["name"]: row for row in tensors}
|
||||
if set(actual) != set(recorded):
|
||||
raise ValueError("inventory tensor names do not match the GGUF reader")
|
||||
for name, value in actual.items():
|
||||
if any(value[field] != recorded[name].get(field) for field in value):
|
||||
raise ValueError(f"inventory metadata mismatch for tensor {name}")
|
||||
ordered = sorted(tensors, key=lambda row: int(row["offset"]))
|
||||
previous_end = 0
|
||||
for row in ordered:
|
||||
offset = int(row["offset"])
|
||||
end = offset + int(row["nbytes"])
|
||||
if offset < previous_end or end > source.stat().st_size:
|
||||
raise ValueError(f"invalid or overlapping source range for {row['name']}")
|
||||
previous_end = end
|
||||
|
||||
|
||||
def generation(model_digest: str, source_digest: str, layer: int, expert: int) -> int:
|
||||
value = hashlib.sha256(
|
||||
f"{model_digest}:{source_digest}:{layer}:{expert}".encode("ascii")
|
||||
).digest()
|
||||
return int.from_bytes(value[:8], "little") or 1
|
||||
|
||||
|
||||
def tool_sha256() -> str:
|
||||
digest = hashlib.sha256()
|
||||
for path in (Path(__file__), Path(__file__).with_name("format.py")):
|
||||
digest.update(path.name.encode("ascii") + b"\0")
|
||||
digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def git_sha() -> str:
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=Path(__file__).resolve().parent,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def build(args: argparse.Namespace) -> dict[str, object]:
|
||||
started_at = now()
|
||||
started_monotonic = time.monotonic()
|
||||
source = args.source.resolve(strict=True)
|
||||
output = args.output.resolve()
|
||||
manifest_path = args.manifest.resolve()
|
||||
checkpoint_path = args.checkpoint.resolve()
|
||||
partial_path = output.with_name(output.name + ".partial")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if output.exists():
|
||||
raise ValueError(f"completed output already exists: {output}")
|
||||
if args.config_blob is not None:
|
||||
if sha256_file(args.config_blob.resolve(strict=True)) != args.config_sha256:
|
||||
raise ValueError("DeepSeek config hash does not match its digest")
|
||||
|
||||
actual_source_sha256 = sha256_file(source)
|
||||
if actual_source_sha256 != args.source_sha256:
|
||||
raise ValueError(
|
||||
f"source SHA-256 mismatch: expected {args.source_sha256}, got {actual_source_sha256}"
|
||||
)
|
||||
|
||||
if args.inventory is None:
|
||||
inventory_header, tensors, inventory_summary = create_inventory(
|
||||
source, actual_source_sha256
|
||||
)
|
||||
else:
|
||||
inventory_header, tensors, inventory_summary = load_inventory(
|
||||
args.inventory.resolve(strict=True), source, actual_source_sha256
|
||||
)
|
||||
validate_inventory_against_reader(source, tensors)
|
||||
|
||||
expert_tensors: dict[tuple[int, str], dict] = {}
|
||||
for row in tensors:
|
||||
match = EXPERT_RE.fullmatch(row["name"])
|
||||
if match is not None:
|
||||
key = int(match.group("layer")), match.group("role")
|
||||
if key in expert_tensors:
|
||||
raise ValueError(f"duplicate routed-expert tensor {key}")
|
||||
expert_tensors[key] = row
|
||||
expected_tensor_keys = {
|
||||
(layer, role) for layer in range(args.num_layers) for role in ROLE_NAMES
|
||||
}
|
||||
if set(expert_tensors) != expected_tensor_keys:
|
||||
missing = sorted(expected_tensor_keys - set(expert_tensors))
|
||||
extra = sorted(set(expert_tensors) - expected_tensor_keys)
|
||||
raise ValueError(
|
||||
f"routed-expert tensor coverage mismatch: missing={missing[:8]} extra={extra[:8]}"
|
||||
)
|
||||
|
||||
role_bytes = set()
|
||||
for (layer, role), row in expert_tensors.items():
|
||||
shape = [int(value) for value in row["shape"]]
|
||||
if len(shape) != 3 or shape[-1] != args.num_experts:
|
||||
raise ValueError(
|
||||
f"unexpected expert shape for layer={layer} role={role}: {shape}"
|
||||
)
|
||||
if int(row["nbytes"]) % args.num_experts:
|
||||
raise ValueError(f"expert tensor is not evenly sliceable: {row['name']}")
|
||||
slice_bytes = int(row["nbytes"]) // args.num_experts
|
||||
block_size, type_size = gguf.GGML_QUANT_SIZES[
|
||||
gguf.GGMLQuantizationType(int(row["type_id"]))
|
||||
]
|
||||
logical_elements = 1
|
||||
for dimension in shape[:-1]:
|
||||
logical_elements *= dimension
|
||||
expected_bytes = logical_elements // block_size * type_size
|
||||
if logical_elements % block_size or slice_bytes != expected_bytes:
|
||||
raise ValueError(
|
||||
f"quantized slice size mismatch for {row['name']}: {slice_bytes} != {expected_bytes}"
|
||||
)
|
||||
role_bytes.add(slice_bytes)
|
||||
if len(role_bytes) != 1:
|
||||
raise ValueError(
|
||||
f"triplet v1 requires uniform role sizes, got {sorted(role_bytes)}"
|
||||
)
|
||||
role_nbytes = role_bytes.pop()
|
||||
|
||||
object_count = args.num_layers * args.num_experts
|
||||
object_payload_bytes = role_nbytes * len(ROLE_NAMES)
|
||||
object_stride = align_up(object_payload_bytes, args.alignment)
|
||||
index_count = object_count * len(ROLE_NAMES)
|
||||
data_start = align_up(
|
||||
HEADER_STRUCT.size + index_count * ENTRY_STRUCT.size, args.alignment
|
||||
)
|
||||
expected_pack_bytes = data_start + object_count * object_stride
|
||||
header = PackHeader(
|
||||
flags=FLAG_IDENTITY_PAYLOAD | FLAG_TRIPLET_OBJECTS,
|
||||
index_count=index_count,
|
||||
data_start=data_start,
|
||||
alignment=args.alignment,
|
||||
num_layers=args.num_layers,
|
||||
num_experts=args.num_experts,
|
||||
top_k=args.top_k,
|
||||
role_count=len(ROLE_NAMES),
|
||||
model_identity_sha256=args.model_identity_sha256,
|
||||
source_blob_sha256=actual_source_sha256,
|
||||
config_sha256=args.config_sha256,
|
||||
)
|
||||
header_raw = header.pack()
|
||||
|
||||
existing_bytes = (
|
||||
partial_path.stat().st_size if args.resume and partial_path.exists() else 0
|
||||
)
|
||||
remaining_bytes = max(expected_pack_bytes - existing_bytes, 0)
|
||||
free_bytes = shutil.disk_usage(output.parent).free
|
||||
safety_bytes = int(args.safety_margin_gib * 1024**3)
|
||||
if free_bytes < remaining_bytes + safety_bytes:
|
||||
raise OSError(
|
||||
f"insufficient free space: free={free_bytes}, remaining_pack={remaining_bytes}, "
|
||||
f"safety={safety_bytes}"
|
||||
)
|
||||
|
||||
if args.resume:
|
||||
checkpoint = json.loads(checkpoint_path.read_text(encoding="utf-8"))
|
||||
if checkpoint.get("status") != "in_progress":
|
||||
raise ValueError("resume checkpoint is not in progress")
|
||||
for field, expected in (
|
||||
("source_sha256", actual_source_sha256),
|
||||
("model_identity_sha256", args.model_identity_sha256),
|
||||
("config_sha256", args.config_sha256),
|
||||
("tool_sha256", tool_sha256()),
|
||||
("expected_pack_bytes", expected_pack_bytes),
|
||||
):
|
||||
if checkpoint.get(field) != expected:
|
||||
raise ValueError(f"resume checkpoint {field} mismatch")
|
||||
completed_layers = [int(value) for value in checkpoint["completed_layers"]]
|
||||
if completed_layers != list(range(len(completed_layers))):
|
||||
raise ValueError("completed layers in checkpoint are not a prefix")
|
||||
entries = [IndexEntry.from_dict(value) for value in checkpoint["entries"]]
|
||||
stream = partial_path.open("r+b", buffering=0)
|
||||
if stream.read(len(header_raw)) != header_raw:
|
||||
raise ValueError("partial pack header does not match the requested build")
|
||||
pack_end = int(checkpoint["pack_end"])
|
||||
if partial_path.stat().st_size < pack_end:
|
||||
raise ValueError("partial pack is shorter than its checkpoint")
|
||||
stream.truncate(pack_end)
|
||||
stream.seek(pack_end)
|
||||
else:
|
||||
if partial_path.exists() or checkpoint_path.exists() or manifest_path.exists():
|
||||
raise ValueError(
|
||||
"build outputs already exist; use --resume for an in-progress build"
|
||||
)
|
||||
completed_layers = []
|
||||
entries: list[IndexEntry] = []
|
||||
stream = partial_path.open("x+b", buffering=0)
|
||||
stream.write(header_raw)
|
||||
stream.truncate(data_start)
|
||||
stream.seek(data_start)
|
||||
checkpoint = {
|
||||
"format": FORMAT + "-checkpoint",
|
||||
"version": 1,
|
||||
"status": "in_progress",
|
||||
"started_at": started_at,
|
||||
"source_sha256": actual_source_sha256,
|
||||
"model_identity_sha256": args.model_identity_sha256,
|
||||
"config_sha256": args.config_sha256,
|
||||
"tool_sha256": tool_sha256(),
|
||||
"expected_pack_bytes": expected_pack_bytes,
|
||||
"completed_layers": [],
|
||||
"pack_end": data_start,
|
||||
"entries": [],
|
||||
}
|
||||
write_json_atomic(checkpoint_path, checkpoint)
|
||||
|
||||
source_fd = os.open(source, os.O_RDONLY)
|
||||
try:
|
||||
for layer in range(len(completed_layers), args.num_layers):
|
||||
layer_entries = []
|
||||
for expert in range(args.num_experts):
|
||||
object_ordinal = layer * args.num_experts + expert
|
||||
object_offset = data_start + object_ordinal * object_stride
|
||||
if stream.tell() != object_offset:
|
||||
raise ValueError(
|
||||
f"pack cursor mismatch: {stream.tell()} != {object_offset}"
|
||||
)
|
||||
object_generation = generation(
|
||||
args.model_identity_sha256, actual_source_sha256, layer, expert
|
||||
)
|
||||
for role in ROLE_NAMES:
|
||||
tensor = expert_tensors[(layer, role)]
|
||||
source_slice_offset = int(tensor["offset"]) + expert * role_nbytes
|
||||
pack_offset = stream.tell()
|
||||
slice_sha256 = copy_range(
|
||||
source_fd, stream, source_slice_offset, role_nbytes
|
||||
)
|
||||
dtype_id = int(tensor["type_id"])
|
||||
block_size = int(
|
||||
gguf.GGML_QUANT_SIZES[gguf.GGMLQuantizationType(dtype_id)][0]
|
||||
)
|
||||
entry = IndexEntry(
|
||||
layer=layer,
|
||||
expert=expert,
|
||||
role=role,
|
||||
dtype_id=dtype_id,
|
||||
dtype=str(tensor["type"]),
|
||||
tensor_name=str(tensor["name"]),
|
||||
source_tensor_offset=int(tensor["offset"]),
|
||||
source_tensor_nbytes=int(tensor["nbytes"]),
|
||||
source_slice_offset=source_slice_offset,
|
||||
source_slice_nbytes=role_nbytes,
|
||||
pack_offset=pack_offset,
|
||||
pack_nbytes=role_nbytes,
|
||||
source_tensor_sha256=str(tensor["sha256"]),
|
||||
source_slice_sha256=slice_sha256,
|
||||
checksum=slice_sha256,
|
||||
shape=tuple(int(value) for value in tensor["shape"][:-1]),
|
||||
quant_scheme=str(tensor["type"]),
|
||||
transform_id="identity-v1",
|
||||
block_size=block_size,
|
||||
generation=object_generation,
|
||||
)
|
||||
entry.pack()
|
||||
layer_entries.append(entry)
|
||||
padding = object_stride - object_payload_bytes
|
||||
if padding:
|
||||
stream.write(bytes(padding))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
entries.extend(layer_entries)
|
||||
checkpoint["completed_layers"].append(layer)
|
||||
checkpoint["pack_end"] = stream.tell()
|
||||
checkpoint["entries"] = [entry.to_dict() for entry in entries]
|
||||
write_json_atomic(checkpoint_path, checkpoint)
|
||||
print(
|
||||
f"completed layer {layer}/{args.num_layers - 1}: pack_end={stream.tell()}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if stream.tell() != expected_pack_bytes:
|
||||
raise ValueError(
|
||||
f"final pack size mismatch: {stream.tell()} != {expected_pack_bytes}"
|
||||
)
|
||||
index_sha256 = write_index(stream, header, entries)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
finally:
|
||||
os.close(source_fd)
|
||||
stream.close()
|
||||
|
||||
pack_sha256 = sha256_file(partial_path)
|
||||
reader = gguf.GGUFReader(source, "r")
|
||||
with source.open("rb", buffering=0) as source_stream:
|
||||
source_metadata_sha256 = hash_range(source_stream, 0, int(reader.data_offset))
|
||||
|
||||
routed_names = {row["name"] for row in expert_tensors.values()}
|
||||
tensor_manifest = []
|
||||
for tensor in sorted(tensors, key=lambda row: row["name"]):
|
||||
routed = tensor["name"] in routed_names
|
||||
tensor_manifest.append(
|
||||
{
|
||||
"name": tensor["name"],
|
||||
"shape": tensor["shape"],
|
||||
"type": tensor["type"],
|
||||
"type_id": tensor["type_id"],
|
||||
"source_offset": tensor["offset"],
|
||||
"source_nbytes": tensor["nbytes"],
|
||||
"source_payload_sha256": tensor["sha256"],
|
||||
"category": "routed_expert" if routed else "non_routed",
|
||||
"mapping": "expert_pack_identity" if routed else "gguf_direct_identity",
|
||||
"scale_storage": (
|
||||
"inline_quant_block"
|
||||
if tensor["type"] == "MXFP4"
|
||||
else "tensor_native"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"format": FORMAT,
|
||||
"version": 1,
|
||||
"complete": True,
|
||||
"created_at": now(),
|
||||
"layout": "triplet_identity",
|
||||
"role_order": list(ROLE_NAMES),
|
||||
"alignment": args.alignment,
|
||||
"pack_path": str(output),
|
||||
"pack_size": expected_pack_bytes,
|
||||
"pack_sha256": pack_sha256,
|
||||
"header_bytes": header.header_bytes,
|
||||
"index_count": index_count,
|
||||
"index_entry_bytes": header.entry_bytes,
|
||||
"index_sha256": index_sha256,
|
||||
"data_start": data_start,
|
||||
"object_count": object_count,
|
||||
"object_payload_bytes": object_payload_bytes,
|
||||
"object_stride": object_stride,
|
||||
"role_bytes": role_nbytes,
|
||||
"payload_bytes": index_count * role_nbytes,
|
||||
"padding_bytes": expected_pack_bytes - data_start - index_count * role_nbytes,
|
||||
"model": {
|
||||
"ref": args.model_ref,
|
||||
"model_identity_sha256": args.model_identity_sha256,
|
||||
"config_sha256": args.config_sha256,
|
||||
"num_layers": args.num_layers,
|
||||
"num_routed_experts": args.num_experts,
|
||||
"top_k": args.top_k,
|
||||
"single_gpu": True,
|
||||
},
|
||||
"source": {
|
||||
"path": str(source),
|
||||
"size": source.stat().st_size,
|
||||
"sha256": actual_source_sha256,
|
||||
"gguf_data_offset": int(reader.data_offset),
|
||||
"gguf_metadata_sha256": source_metadata_sha256,
|
||||
"inventory_path": str(args.inventory.resolve()) if args.inventory else None,
|
||||
"inventory_sha256": inventory_summary.get("inventory_sha256"),
|
||||
"tensor_count": len(tensors),
|
||||
},
|
||||
"coverage": {
|
||||
"layers": list(range(args.num_layers)),
|
||||
"experts_per_layer": args.num_experts,
|
||||
"roles": list(ROLE_NAMES),
|
||||
"routed_tensor_count": len(routed_names),
|
||||
"non_routed_tensor_count": len(tensors) - len(routed_names),
|
||||
},
|
||||
"transform": {
|
||||
"id": "identity-v1",
|
||||
"description": "Contiguous source bytes; no dequantization, requantization, or value transform",
|
||||
"reversible": True,
|
||||
"tool_sha256": tool_sha256(),
|
||||
},
|
||||
"builder": {
|
||||
"git_sha": git_sha(),
|
||||
"python": sys.version,
|
||||
"command": " ".join(sys.argv),
|
||||
"started_at": started_at,
|
||||
"elapsed_s": time.monotonic() - started_monotonic,
|
||||
},
|
||||
"tensors": tensor_manifest,
|
||||
}
|
||||
|
||||
os.replace(partial_path, output)
|
||||
write_json_atomic(manifest_path, manifest)
|
||||
checkpoint["status"] = "complete"
|
||||
checkpoint["completed_at"] = now()
|
||||
checkpoint["manifest_sha256"] = sha256_file(manifest_path)
|
||||
checkpoint["pack_sha256"] = pack_sha256
|
||||
write_json_atomic(checkpoint_path, checkpoint)
|
||||
return manifest
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build an auditable SGLang expert pack from GGUF"
|
||||
)
|
||||
parser.add_argument("--source", type=Path)
|
||||
parser.add_argument("--source-sha256")
|
||||
parser.add_argument("--inventory", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--manifest", type=Path)
|
||||
parser.add_argument("--checkpoint", type=Path)
|
||||
parser.add_argument("--model-ref", default="deepseek-v4-flash")
|
||||
parser.add_argument("--model-identity-sha256")
|
||||
parser.add_argument("--config-blob", type=Path)
|
||||
parser.add_argument("--config-sha256")
|
||||
parser.add_argument("--num-layers", type=int, default=43)
|
||||
parser.add_argument("--num-experts", type=int, default=256)
|
||||
parser.add_argument("--top-k", type=int, default=6)
|
||||
parser.add_argument("--alignment", type=int, default=4096)
|
||||
parser.add_argument("--safety-margin-gib", type=float, default=16.0)
|
||||
parser.add_argument("--resume", action="store_true")
|
||||
parser.add_argument("--inspect", action="store_true")
|
||||
parser.add_argument("--limit", type=int, default=12)
|
||||
args = parser.parse_args()
|
||||
if args.inspect:
|
||||
return args
|
||||
for name in ("source", "source_sha256", "model_identity_sha256", "config_sha256"):
|
||||
if getattr(args, name) is None:
|
||||
parser.error(f"--{name.replace('_', '-')} is required when building")
|
||||
args.manifest = args.manifest or args.output.with_name(
|
||||
args.output.name + ".manifest.json"
|
||||
)
|
||||
args.checkpoint = args.checkpoint or args.output.with_name(
|
||||
args.output.name + ".checkpoint.json"
|
||||
)
|
||||
if args.num_layers <= 0 or args.num_experts <= 0 or args.top_k <= 0:
|
||||
parser.error("model dimensions and top-k must be positive")
|
||||
align_up(0, args.alignment)
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.inspect:
|
||||
inspect_pack(args.output, args.limit)
|
||||
return 0
|
||||
manifest = build(args)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"pack": manifest["pack_path"],
|
||||
"pack_sha256": manifest["pack_sha256"],
|
||||
"manifest": str(args.manifest.resolve()),
|
||||
"objects": manifest["object_count"],
|
||||
"entries": manifest["index_count"],
|
||||
"pack_size": manifest["pack_size"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Iterable
|
||||
|
||||
MAGIC = b"SGLANG-EXPERTPACK-v1\0\0\0\0"
|
||||
VERSION = 1
|
||||
ROLE_NAMES = ("gate", "up", "down")
|
||||
ROLE_IDS = {name: index for index, name in enumerate(ROLE_NAMES)}
|
||||
FLAG_IDENTITY_PAYLOAD = 1 << 0
|
||||
FLAG_TRIPLET_OBJECTS = 1 << 1
|
||||
|
||||
# magic, version, header bytes, entry bytes, flags, index count, data start,
|
||||
# alignment, layer count, expert count, top-k, role count, and three digests.
|
||||
HEADER_STRUCT = struct.Struct("<24sIIIIQQQIIII32s32s32s")
|
||||
|
||||
# layer, expert, role, rank, GGML dtype id, dtype, tensor name, six ranges,
|
||||
# source tensor/slice and pack hashes, logical role shape, quant/transform,
|
||||
# quant block size, and generation.
|
||||
ENTRY_STRUCT = struct.Struct("<HHBBH16s80sQQQQQQ32s32s32s4Q16s16sQQ")
|
||||
|
||||
|
||||
def align_up(value: int, alignment: int) -> int:
|
||||
if alignment <= 0 or alignment & (alignment - 1):
|
||||
raise ValueError("alignment must be a positive power of two")
|
||||
return (value + alignment - 1) // alignment * alignment
|
||||
|
||||
|
||||
def sha256_file(path: Path, chunk_bytes: int = 16 * 1024 * 1024) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb", buffering=0) as stream:
|
||||
while chunk := stream.read(chunk_bytes):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def parse_sha256(value: str, field: str) -> bytes:
|
||||
if len(value) != 64:
|
||||
raise ValueError(f"{field} must be a 64-character SHA-256 digest")
|
||||
try:
|
||||
result = bytes.fromhex(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{field} must be a hexadecimal SHA-256 digest") from exc
|
||||
if len(result) != 32:
|
||||
raise ValueError(f"{field} must decode to 32 bytes")
|
||||
return result
|
||||
|
||||
|
||||
def encode_fixed(value: str, size: int, field: str) -> bytes:
|
||||
encoded = value.encode("utf-8")
|
||||
if len(encoded) >= size:
|
||||
raise ValueError(f"{field} is too long for its {size}-byte field: {value!r}")
|
||||
return encoded + bytes(size - len(encoded))
|
||||
|
||||
|
||||
def decode_fixed(value: bytes) -> str:
|
||||
return value.split(b"\0", 1)[0].decode("utf-8")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackHeader:
|
||||
flags: int
|
||||
index_count: int
|
||||
data_start: int
|
||||
alignment: int
|
||||
num_layers: int
|
||||
num_experts: int
|
||||
top_k: int
|
||||
role_count: int
|
||||
model_identity_sha256: str
|
||||
source_blob_sha256: str
|
||||
config_sha256: str
|
||||
|
||||
@property
|
||||
def header_bytes(self) -> int:
|
||||
return HEADER_STRUCT.size
|
||||
|
||||
@property
|
||||
def entry_bytes(self) -> int:
|
||||
return ENTRY_STRUCT.size
|
||||
|
||||
def pack(self) -> bytes:
|
||||
if self.role_count != len(ROLE_NAMES):
|
||||
raise ValueError(f"role_count must be {len(ROLE_NAMES)}")
|
||||
expected_entries = self.num_layers * self.num_experts * self.role_count
|
||||
if self.index_count != expected_entries:
|
||||
raise ValueError(
|
||||
f"index_count {self.index_count} does not match {expected_entries}"
|
||||
)
|
||||
minimum_data_start = self.header_bytes + self.index_count * self.entry_bytes
|
||||
if self.data_start < minimum_data_start or self.data_start % self.alignment:
|
||||
raise ValueError("data_start is too small or is not aligned")
|
||||
return HEADER_STRUCT.pack(
|
||||
MAGIC,
|
||||
VERSION,
|
||||
self.header_bytes,
|
||||
self.entry_bytes,
|
||||
self.flags,
|
||||
self.index_count,
|
||||
self.data_start,
|
||||
self.alignment,
|
||||
self.num_layers,
|
||||
self.num_experts,
|
||||
self.top_k,
|
||||
self.role_count,
|
||||
parse_sha256(self.model_identity_sha256, "model_identity_sha256"),
|
||||
parse_sha256(self.source_blob_sha256, "source_blob_sha256"),
|
||||
parse_sha256(self.config_sha256, "config_sha256"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, raw: bytes) -> PackHeader:
|
||||
if len(raw) != HEADER_STRUCT.size:
|
||||
raise ValueError("expert-pack header is truncated")
|
||||
(
|
||||
magic,
|
||||
version,
|
||||
header_bytes,
|
||||
entry_bytes,
|
||||
flags,
|
||||
index_count,
|
||||
data_start,
|
||||
alignment,
|
||||
num_layers,
|
||||
num_experts,
|
||||
top_k,
|
||||
role_count,
|
||||
model_identity_digest,
|
||||
source_digest,
|
||||
config_digest,
|
||||
) = HEADER_STRUCT.unpack(raw)
|
||||
if magic != MAGIC:
|
||||
raise ValueError("expert-pack magic does not match")
|
||||
if version != VERSION:
|
||||
raise ValueError(f"unsupported expert-pack version {version}")
|
||||
if header_bytes != HEADER_STRUCT.size or entry_bytes != ENTRY_STRUCT.size:
|
||||
raise ValueError(
|
||||
"expert-pack struct sizes do not match this implementation"
|
||||
)
|
||||
result = cls(
|
||||
flags=flags,
|
||||
index_count=index_count,
|
||||
data_start=data_start,
|
||||
alignment=alignment,
|
||||
num_layers=num_layers,
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
role_count=role_count,
|
||||
model_identity_sha256=model_identity_digest.hex(),
|
||||
source_blob_sha256=source_digest.hex(),
|
||||
config_sha256=config_digest.hex(),
|
||||
)
|
||||
result.pack()
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IndexEntry:
|
||||
layer: int
|
||||
expert: int
|
||||
role: str
|
||||
dtype_id: int
|
||||
dtype: str
|
||||
tensor_name: str
|
||||
source_tensor_offset: int
|
||||
source_tensor_nbytes: int
|
||||
source_slice_offset: int
|
||||
source_slice_nbytes: int
|
||||
pack_offset: int
|
||||
pack_nbytes: int
|
||||
source_tensor_sha256: str
|
||||
source_slice_sha256: str
|
||||
checksum: str
|
||||
shape: tuple[int, ...]
|
||||
quant_scheme: str
|
||||
transform_id: str
|
||||
block_size: int
|
||||
generation: int
|
||||
|
||||
@property
|
||||
def key(self) -> tuple[int, int, int]:
|
||||
return self.layer, self.expert, ROLE_IDS[self.role]
|
||||
|
||||
def pack(self) -> bytes:
|
||||
if self.role not in ROLE_IDS:
|
||||
raise ValueError(f"unknown expert role {self.role!r}")
|
||||
if not 0 <= self.layer <= 0xFFFF or not 0 <= self.expert <= 0xFFFF:
|
||||
raise ValueError("layer/expert does not fit the pack index")
|
||||
if not 0 <= self.dtype_id <= 0xFFFF:
|
||||
raise ValueError("dtype_id does not fit the pack index")
|
||||
if not 1 <= len(self.shape) <= 4 or any(value <= 0 for value in self.shape):
|
||||
raise ValueError(f"invalid role shape {self.shape}")
|
||||
dims = self.shape + (0,) * (4 - len(self.shape))
|
||||
return ENTRY_STRUCT.pack(
|
||||
self.layer,
|
||||
self.expert,
|
||||
ROLE_IDS[self.role],
|
||||
len(self.shape),
|
||||
self.dtype_id,
|
||||
encode_fixed(self.dtype, 16, "dtype"),
|
||||
encode_fixed(self.tensor_name, 80, "tensor_name"),
|
||||
self.source_tensor_offset,
|
||||
self.source_tensor_nbytes,
|
||||
self.source_slice_offset,
|
||||
self.source_slice_nbytes,
|
||||
self.pack_offset,
|
||||
self.pack_nbytes,
|
||||
parse_sha256(self.source_tensor_sha256, "source_tensor_sha256"),
|
||||
parse_sha256(self.source_slice_sha256, "source_slice_sha256"),
|
||||
parse_sha256(self.checksum, "checksum"),
|
||||
*dims,
|
||||
encode_fixed(self.quant_scheme, 16, "quant_scheme"),
|
||||
encode_fixed(self.transform_id, 16, "transform_id"),
|
||||
self.block_size,
|
||||
self.generation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, raw: bytes) -> IndexEntry:
|
||||
if len(raw) != ENTRY_STRUCT.size:
|
||||
raise ValueError("expert-pack index entry is truncated")
|
||||
values = ENTRY_STRUCT.unpack(raw)
|
||||
role_id = values[2]
|
||||
rank = values[3]
|
||||
if role_id >= len(ROLE_NAMES) or not 1 <= rank <= 4:
|
||||
raise ValueError("expert-pack index contains an invalid role or rank")
|
||||
shape = tuple(values[16 : 16 + rank])
|
||||
return cls(
|
||||
layer=values[0],
|
||||
expert=values[1],
|
||||
role=ROLE_NAMES[role_id],
|
||||
dtype_id=values[4],
|
||||
dtype=decode_fixed(values[5]),
|
||||
tensor_name=decode_fixed(values[6]),
|
||||
source_tensor_offset=values[7],
|
||||
source_tensor_nbytes=values[8],
|
||||
source_slice_offset=values[9],
|
||||
source_slice_nbytes=values[10],
|
||||
pack_offset=values[11],
|
||||
pack_nbytes=values[12],
|
||||
source_tensor_sha256=values[13].hex(),
|
||||
source_slice_sha256=values[14].hex(),
|
||||
checksum=values[15].hex(),
|
||||
shape=shape,
|
||||
quant_scheme=decode_fixed(values[20]),
|
||||
transform_id=decode_fixed(values[21]),
|
||||
block_size=values[22],
|
||||
generation=values[23],
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
value = dict(self.__dict__)
|
||||
value["shape"] = list(self.shape)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: dict[str, object]) -> IndexEntry:
|
||||
fields = dict(value)
|
||||
fields["shape"] = tuple(int(item) for item in fields["shape"])
|
||||
return cls(**fields)
|
||||
|
||||
|
||||
def read_header(stream: BinaryIO) -> PackHeader:
|
||||
stream.seek(0)
|
||||
return PackHeader.unpack(stream.read(HEADER_STRUCT.size))
|
||||
|
||||
|
||||
def read_index(stream: BinaryIO, header: PackHeader) -> list[IndexEntry]:
|
||||
stream.seek(header.header_bytes)
|
||||
entries = []
|
||||
for _ in range(header.index_count):
|
||||
entries.append(IndexEntry.unpack(stream.read(header.entry_bytes)))
|
||||
return entries
|
||||
|
||||
|
||||
def write_index(
|
||||
stream: BinaryIO, header: PackHeader, entries: Iterable[IndexEntry]
|
||||
) -> str:
|
||||
ordered = sorted(entries, key=lambda entry: entry.key)
|
||||
if len(ordered) != header.index_count:
|
||||
raise ValueError(
|
||||
f"expected {header.index_count} index entries, got {len(ordered)}"
|
||||
)
|
||||
digest = hashlib.sha256()
|
||||
stream.seek(header.header_bytes)
|
||||
for entry in ordered:
|
||||
raw = entry.pack()
|
||||
stream.write(raw)
|
||||
digest.update(raw)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def inspect_pack(path: Path, limit: int = 12) -> dict[str, object]:
|
||||
with path.open("rb", buffering=0) as stream:
|
||||
header = read_header(stream)
|
||||
entries = read_index(stream, header)
|
||||
summary = {
|
||||
"path": str(path.resolve()),
|
||||
"size": path.stat().st_size,
|
||||
"header": header.__dict__,
|
||||
"role_counts": {
|
||||
role: sum(entry.role == role for entry in entries) for role in ROLE_NAMES
|
||||
},
|
||||
"payload_bytes": sum(entry.pack_nbytes for entry in entries),
|
||||
"entries": [entry.to_dict() for entry in entries[:limit]],
|
||||
}
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return summary
|
||||
+738
@@ -0,0 +1,738 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Structural inventory and adapter manifest for Kimi K3 GGUF expert packs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
from collections.abc import Callable, Iterable
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO
|
||||
|
||||
FORMAT = "SGLANG-KIMI-GGMLMOEPACK-ADAPTER-v1"
|
||||
PACK_MAGIC = b"GGMLMOEPACKv1\0\0\0"
|
||||
PACK_VERSION = 1
|
||||
PACK_HEADER = struct.Struct("<16sIIQQ")
|
||||
PACK_ENTRY = struct.Struct("<128siIQQ")
|
||||
PACK_ALIGNMENT = 4096
|
||||
ROLE_ORDER = ("up", "gate", "down")
|
||||
EXPERT_RE = re.compile(
|
||||
r"^blk\.(?P<layer>\d+)\.ffn_(?P<role>up|gate|down)_exps\.weight$"
|
||||
)
|
||||
SHARD_RE = re.compile(r"-(?P<number>\d{5})-of-(?P<count>\d{5})\.gguf$")
|
||||
COPY_CHUNK_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KimiK3Spec:
|
||||
num_hidden_layers: int
|
||||
first_k_dense_replace: int
|
||||
num_experts: int
|
||||
top_k: int
|
||||
num_shared_experts: int
|
||||
hidden_size: int
|
||||
routed_expert_hidden_size: int
|
||||
moe_intermediate_size: int
|
||||
hidden_act: str
|
||||
active_moe_layer_ids: tuple[int, ...]
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, Any]) -> KimiK3Spec:
|
||||
text_config = config.get("text_config", config)
|
||||
num_hidden_layers = int(text_config["num_hidden_layers"])
|
||||
first_dense = int(text_config["first_k_dense_replace"])
|
||||
active_layers = tuple(range(first_dense, num_hidden_layers))
|
||||
result = cls(
|
||||
num_hidden_layers=num_hidden_layers,
|
||||
first_k_dense_replace=first_dense,
|
||||
num_experts=int(text_config["num_experts"]),
|
||||
top_k=int(text_config["num_experts_per_token"]),
|
||||
num_shared_experts=int(text_config["num_shared_experts"]),
|
||||
hidden_size=int(text_config["hidden_size"]),
|
||||
routed_expert_hidden_size=int(text_config["routed_expert_hidden_size"]),
|
||||
moe_intermediate_size=int(text_config["moe_intermediate_size"]),
|
||||
hidden_act=str(text_config["hidden_act"]),
|
||||
active_moe_layer_ids=active_layers,
|
||||
)
|
||||
result.validate_kimi_k3()
|
||||
return result
|
||||
|
||||
def validate_kimi_k3(self) -> None:
|
||||
expected = {
|
||||
"num_hidden_layers": 93,
|
||||
"first_k_dense_replace": 1,
|
||||
"num_experts": 896,
|
||||
"top_k": 16,
|
||||
"num_shared_experts": 2,
|
||||
"hidden_size": 7168,
|
||||
"routed_expert_hidden_size": 3584,
|
||||
"moe_intermediate_size": 3072,
|
||||
"hidden_act": "situ",
|
||||
}
|
||||
actual = {
|
||||
"num_hidden_layers": self.num_hidden_layers,
|
||||
"first_k_dense_replace": self.first_k_dense_replace,
|
||||
"num_experts": self.num_experts,
|
||||
"top_k": self.top_k,
|
||||
"num_shared_experts": self.num_shared_experts,
|
||||
"hidden_size": self.hidden_size,
|
||||
"routed_expert_hidden_size": self.routed_expert_hidden_size,
|
||||
"moe_intermediate_size": self.moe_intermediate_size,
|
||||
"hidden_act": self.hidden_act,
|
||||
}
|
||||
if actual != expected:
|
||||
raise ValueError(
|
||||
"Kimi K3 model invariants do not match the audited model: "
|
||||
f"expected={expected}, actual={actual}"
|
||||
)
|
||||
expected_layers = tuple(range(1, 93))
|
||||
if self.active_moe_layer_ids != expected_layers:
|
||||
raise ValueError("Kimi K3 active MoE layers must be exactly 1..92")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TensorRecord:
|
||||
name: str
|
||||
shape: tuple[int, ...]
|
||||
dtype: str
|
||||
dtype_id: int
|
||||
shard_index: int
|
||||
shard_path: str
|
||||
data_offset: int
|
||||
nbytes: int
|
||||
|
||||
@property
|
||||
def expert_key(self) -> tuple[int, str] | None:
|
||||
match = EXPERT_RE.fullmatch(self.name)
|
||||
if match is None:
|
||||
return None
|
||||
return int(match.group("layer")), match.group("role")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackEntryRecord:
|
||||
tensor_name: str
|
||||
expert: int
|
||||
offset: int
|
||||
nbytes: int
|
||||
|
||||
|
||||
def _field_value(reader: Any, name: str) -> Any:
|
||||
field = reader.fields.get(name)
|
||||
if field is None:
|
||||
raise ValueError(f"GGUF metadata is missing required field {name!r}")
|
||||
return field.contents()
|
||||
|
||||
|
||||
def _sha256_range(stream: BinaryIO, offset: int, nbytes: int) -> str:
|
||||
digest = hashlib.sha256()
|
||||
stream.seek(offset)
|
||||
remaining = nbytes
|
||||
while remaining:
|
||||
chunk = stream.read(min(COPY_CHUNK_BYTES, remaining))
|
||||
if not chunk:
|
||||
raise EOFError(f"short read at offset {offset}; {remaining} bytes remain")
|
||||
digest.update(chunk)
|
||||
remaining -= len(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
with path.open("rb", buffering=0) as stream:
|
||||
return _sha256_range(stream, 0, path.stat().st_size)
|
||||
|
||||
|
||||
def canonical_sha256(value: Any) -> str:
|
||||
encoded = json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def write_json_atomic(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
with temporary.open("w", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def discover_gguf_shards(directory: Path) -> list[Path]:
|
||||
directory = directory.resolve(strict=True)
|
||||
candidates = sorted(directory.glob("*.gguf"))
|
||||
if not candidates:
|
||||
raise ValueError(f"no GGUF files found in {directory}")
|
||||
numbered: list[tuple[int, int, Path]] = []
|
||||
for path in candidates:
|
||||
match = SHARD_RE.search(path.name)
|
||||
if match is None:
|
||||
raise ValueError(f"GGUF shard name does not contain NNNNN-of-NNNNN: {path}")
|
||||
numbered.append((int(match.group("number")), int(match.group("count")), path))
|
||||
counts = {count for _, count, _ in numbered}
|
||||
if len(counts) != 1:
|
||||
raise ValueError(f"GGUF shard filenames disagree on split count: {counts}")
|
||||
count = counts.pop()
|
||||
numbers = [number for number, _, _ in numbered]
|
||||
if count != len(numbered) or sorted(numbers) != list(range(1, count + 1)):
|
||||
raise ValueError(
|
||||
f"GGUF shard set is incomplete: count={count}, numbers={sorted(numbers)}"
|
||||
)
|
||||
return [path for _, _, path in sorted(numbered)]
|
||||
|
||||
|
||||
def _git_sha(repo: Path | None) -> str:
|
||||
if repo is None:
|
||||
return "unknown"
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def scan_gguf_shards(
|
||||
paths: list[Path], *, full_source_hashes: bool = False
|
||||
) -> tuple[list[dict[str, Any]], list[TensorRecord], dict[str, Any]]:
|
||||
import gguf
|
||||
|
||||
shard_records: list[dict[str, Any]] = []
|
||||
tensors: list[TensorRecord] = []
|
||||
seen_names: set[str] = set()
|
||||
split_count: int | None = None
|
||||
split_tensor_count: int | None = None
|
||||
architecture: str | None = None
|
||||
for shard_index, path in enumerate(paths):
|
||||
reader = gguf.GGUFReader(str(path), mode="r")
|
||||
shard_split_count = int(_field_value(reader, "split.count"))
|
||||
shard_split_no = int(_field_value(reader, "split.no"))
|
||||
shard_tensor_count = int(_field_value(reader, "split.tensors.count"))
|
||||
shard_architecture = str(_field_value(reader, "general.architecture"))
|
||||
if shard_split_no != shard_index:
|
||||
raise ValueError(
|
||||
f"GGUF split.no mismatch for {path}: {shard_split_no} != {shard_index}"
|
||||
)
|
||||
if split_count is None:
|
||||
split_count = shard_split_count
|
||||
split_tensor_count = shard_tensor_count
|
||||
architecture = shard_architecture
|
||||
elif (
|
||||
split_count != shard_split_count
|
||||
or split_tensor_count != shard_tensor_count
|
||||
or architecture != shard_architecture
|
||||
):
|
||||
raise ValueError(f"GGUF split metadata mismatch at {path}")
|
||||
|
||||
metadata_nbytes = int(reader.data_offset)
|
||||
with path.open("rb", buffering=0) as stream:
|
||||
metadata_sha256 = _sha256_range(stream, 0, metadata_nbytes)
|
||||
shard_record: dict[str, Any] = {
|
||||
"index": shard_index,
|
||||
"path": str(path.resolve()),
|
||||
"size": path.stat().st_size,
|
||||
"metadata_nbytes": metadata_nbytes,
|
||||
"metadata_sha256": metadata_sha256,
|
||||
"tensor_count": len(reader.tensors),
|
||||
}
|
||||
if full_source_hashes:
|
||||
shard_record["sha256"] = sha256_file(path)
|
||||
shard_records.append(shard_record)
|
||||
|
||||
for tensor in reader.tensors:
|
||||
if tensor.name in seen_names:
|
||||
raise ValueError(f"duplicate GGUF tensor across shards: {tensor.name}")
|
||||
seen_names.add(tensor.name)
|
||||
tensors.append(
|
||||
TensorRecord(
|
||||
name=tensor.name,
|
||||
shape=tuple(int(value) for value in tensor.shape.tolist()),
|
||||
dtype=tensor.tensor_type.name,
|
||||
dtype_id=int(tensor.tensor_type),
|
||||
shard_index=shard_index,
|
||||
shard_path=str(path.resolve()),
|
||||
data_offset=int(tensor.data_offset),
|
||||
nbytes=int(tensor.n_bytes),
|
||||
)
|
||||
)
|
||||
del reader
|
||||
|
||||
if split_count != len(paths):
|
||||
raise ValueError(f"GGUF split.count={split_count}, found {len(paths)} files")
|
||||
if split_tensor_count != len(tensors):
|
||||
raise ValueError(
|
||||
f"GGUF split.tensors.count={split_tensor_count}, found {len(tensors)}"
|
||||
)
|
||||
if architecture != "kimi-k3":
|
||||
raise ValueError(f"expected GGUF architecture 'kimi-k3', got {architecture!r}")
|
||||
summary = {
|
||||
"architecture": architecture,
|
||||
"shard_count": len(paths),
|
||||
"tensor_count": len(tensors),
|
||||
"total_bytes": sum(item["size"] for item in shard_records),
|
||||
"full_source_hashes": full_source_hashes,
|
||||
}
|
||||
return shard_records, tensors, summary
|
||||
|
||||
|
||||
def validate_expert_tensors(
|
||||
tensors: Iterable[TensorRecord], spec: KimiK3Spec
|
||||
) -> dict[tuple[int, str], TensorRecord]:
|
||||
expert_tensors: dict[tuple[int, str], TensorRecord] = {}
|
||||
for tensor in tensors:
|
||||
key = tensor.expert_key
|
||||
if key is None:
|
||||
continue
|
||||
if key in expert_tensors:
|
||||
raise ValueError(f"duplicate routed expert tensor {key}")
|
||||
expert_tensors[key] = tensor
|
||||
expected_keys = {
|
||||
(layer, role) for layer in spec.active_moe_layer_ids for role in ROLE_ORDER
|
||||
}
|
||||
if set(expert_tensors) != expected_keys:
|
||||
missing = sorted(expected_keys - set(expert_tensors))
|
||||
extra = sorted(set(expert_tensors) - expected_keys)
|
||||
raise ValueError(
|
||||
f"routed expert tensor coverage mismatch: missing={missing[:8]}, "
|
||||
f"extra={extra[:8]}"
|
||||
)
|
||||
expected_layout = {
|
||||
"up": ((spec.routed_expert_hidden_size, spec.moe_intermediate_size), "Q2_K"),
|
||||
"gate": (
|
||||
(spec.routed_expert_hidden_size, spec.moe_intermediate_size),
|
||||
"Q2_K",
|
||||
),
|
||||
"down": (
|
||||
(spec.moe_intermediate_size, spec.routed_expert_hidden_size),
|
||||
"Q3_K",
|
||||
),
|
||||
}
|
||||
for (layer, role), tensor in expert_tensors.items():
|
||||
expected_shape, expected_dtype = expected_layout[role]
|
||||
if tensor.shape != (*expected_shape, spec.num_experts):
|
||||
raise ValueError(
|
||||
f"unexpected expert shape for {(layer, role)}: {tensor.shape}"
|
||||
)
|
||||
if tensor.dtype != expected_dtype:
|
||||
raise ValueError(
|
||||
f"unexpected expert dtype for {(layer, role)}: {tensor.dtype}"
|
||||
)
|
||||
if tensor.nbytes % spec.num_experts:
|
||||
raise ValueError(f"expert tensor is not evenly sliceable: {tensor.name}")
|
||||
return expert_tensors
|
||||
|
||||
|
||||
def _decode_name(raw: bytes) -> str:
|
||||
try:
|
||||
return raw.split(b"\0", 1)[0].decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ValueError("expert-pack tensor name is not valid UTF-8") from exc
|
||||
|
||||
|
||||
def _read_pack_entry(stream: BinaryIO, digest: hashlib._Hash) -> PackEntryRecord:
|
||||
raw = stream.read(PACK_ENTRY.size)
|
||||
if len(raw) != PACK_ENTRY.size:
|
||||
raise ValueError("GGML expert-pack index is truncated")
|
||||
digest.update(raw)
|
||||
name, expert, reserved, offset, nbytes = PACK_ENTRY.unpack(raw)
|
||||
if reserved != 0:
|
||||
raise ValueError("GGML expert-pack reserved entry field must be zero")
|
||||
return PackEntryRecord(_decode_name(name), expert, offset, nbytes)
|
||||
|
||||
|
||||
def _compare_ranges(
|
||||
pack_stream: BinaryIO,
|
||||
pack_entry: PackEntryRecord,
|
||||
tensor: TensorRecord,
|
||||
expert_bytes: int,
|
||||
) -> None:
|
||||
source_offset = tensor.data_offset + pack_entry.expert * expert_bytes
|
||||
with Path(tensor.shard_path).open("rb", buffering=0) as source_stream:
|
||||
source_stream.seek(source_offset)
|
||||
pack_stream.seek(pack_entry.offset)
|
||||
remaining = expert_bytes
|
||||
while remaining:
|
||||
size = min(COPY_CHUNK_BYTES, remaining)
|
||||
source_chunk = source_stream.read(size)
|
||||
pack_chunk = pack_stream.read(size)
|
||||
if source_chunk != pack_chunk or len(source_chunk) != size:
|
||||
raise ValueError(
|
||||
"expert-pack payload does not match GGUF source for "
|
||||
f"{pack_entry.tensor_name} expert {pack_entry.expert}"
|
||||
)
|
||||
remaining -= size
|
||||
|
||||
|
||||
def validate_ggml_moe_pack(
|
||||
path: Path,
|
||||
expert_tensors: dict[tuple[int, str], TensorRecord],
|
||||
spec: KimiK3Spec,
|
||||
*,
|
||||
payload_samples: int = 6,
|
||||
full_pack_hash: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
path = path.resolve(strict=True)
|
||||
expected_count = len(spec.active_moe_layer_ids) * spec.num_experts * len(ROLE_ORDER)
|
||||
file_size = path.stat().st_size
|
||||
index_digest = hashlib.sha256()
|
||||
role_summary: dict[str, dict[str, Any]] = {}
|
||||
sample_indices = set()
|
||||
if payload_samples > 0:
|
||||
if payload_samples == 1:
|
||||
sample_indices.add(0)
|
||||
else:
|
||||
sample_indices.update(
|
||||
round(index * (expected_count - 1) / (payload_samples - 1))
|
||||
for index in range(payload_samples)
|
||||
)
|
||||
sampled_entries: list[tuple[PackEntryRecord, TensorRecord, int]] = []
|
||||
object_bytes: int | None = None
|
||||
object_start: int | None = None
|
||||
previous_end = 0
|
||||
|
||||
with path.open("rb", buffering=0) as stream:
|
||||
raw_header = stream.read(PACK_HEADER.size)
|
||||
if len(raw_header) != PACK_HEADER.size:
|
||||
raise ValueError("GGML expert-pack header is truncated")
|
||||
index_digest.update(raw_header)
|
||||
magic, version, header_size, index_count, data_start = PACK_HEADER.unpack(
|
||||
raw_header
|
||||
)
|
||||
if magic != PACK_MAGIC or version != PACK_VERSION:
|
||||
raise ValueError("GGML expert-pack magic or version does not match")
|
||||
if header_size != PACK_HEADER.size:
|
||||
raise ValueError("GGML expert-pack header size does not match")
|
||||
if index_count != expected_count:
|
||||
raise ValueError(
|
||||
f"GGML expert-pack has {index_count} entries; expected {expected_count}"
|
||||
)
|
||||
minimum_data_start = PACK_HEADER.size + index_count * PACK_ENTRY.size
|
||||
if data_start < minimum_data_start or data_start % PACK_ALIGNMENT:
|
||||
raise ValueError("GGML expert-pack data_start is invalid or unaligned")
|
||||
previous_end = data_start
|
||||
|
||||
for index in range(index_count):
|
||||
entry = _read_pack_entry(stream, index_digest)
|
||||
object_index, physical_role_id = divmod(index, len(ROLE_ORDER))
|
||||
active_layer_index, expected_expert = divmod(object_index, spec.num_experts)
|
||||
expected_layer = spec.active_moe_layer_ids[active_layer_index]
|
||||
expected_role = ROLE_ORDER[physical_role_id]
|
||||
match = EXPERT_RE.fullmatch(entry.tensor_name)
|
||||
if match is None:
|
||||
raise ValueError(f"invalid expert tensor name: {entry.tensor_name!r}")
|
||||
actual_key = (int(match.group("layer")), match.group("role"))
|
||||
expected_key = (expected_layer, expected_role)
|
||||
if actual_key != expected_key or entry.expert != expected_expert:
|
||||
raise ValueError(
|
||||
"GGML expert-pack is not complete expert-major up/gate/down "
|
||||
f"layout at index {index}: expected={(*expected_key, expected_expert)}, "
|
||||
f"actual={(*actual_key, entry.expert)}"
|
||||
)
|
||||
tensor = expert_tensors[expected_key]
|
||||
expert_bytes = tensor.nbytes // spec.num_experts
|
||||
if entry.nbytes != expert_bytes:
|
||||
raise ValueError(
|
||||
f"expert-pack byte size mismatch for index {index}: "
|
||||
f"{entry.nbytes} != {expert_bytes}"
|
||||
)
|
||||
if entry.offset % PACK_ALIGNMENT:
|
||||
raise ValueError(f"expert-pack entry {index} is not 4 KiB aligned")
|
||||
if entry.offset < previous_end or entry.offset + entry.nbytes > file_size:
|
||||
raise ValueError(
|
||||
f"expert-pack entry {index} overlaps or is out of range"
|
||||
)
|
||||
previous_end = entry.offset + entry.nbytes
|
||||
summary = role_summary.setdefault(
|
||||
expected_role,
|
||||
{
|
||||
"dtype": tensor.dtype,
|
||||
"dtype_id": tensor.dtype_id,
|
||||
"logical_shape": list(tensor.shape[:2]),
|
||||
"expert_bytes": expert_bytes,
|
||||
"entry_count": 0,
|
||||
"payload_bytes": 0,
|
||||
},
|
||||
)
|
||||
if summary["expert_bytes"] != expert_bytes:
|
||||
raise ValueError(f"variable expert bytes for role {expected_role}")
|
||||
summary["entry_count"] += 1
|
||||
summary["payload_bytes"] += entry.nbytes
|
||||
|
||||
if physical_role_id == 0:
|
||||
object_start = entry.offset
|
||||
elif physical_role_id == len(ROLE_ORDER) - 1:
|
||||
assert object_start is not None
|
||||
span = entry.offset + entry.nbytes - object_start
|
||||
if object_bytes is None:
|
||||
object_bytes = span
|
||||
elif object_bytes != span:
|
||||
raise ValueError("expert-pack object spans are not fixed size")
|
||||
if index in sample_indices:
|
||||
sampled_entries.append((entry, tensor, expert_bytes))
|
||||
|
||||
if previous_end != file_size:
|
||||
raise ValueError(
|
||||
f"expert-pack has unexplained trailing bytes: {file_size - previous_end}"
|
||||
)
|
||||
for entry, tensor, expert_bytes in sampled_entries:
|
||||
_compare_ranges(stream, entry, tensor, expert_bytes)
|
||||
|
||||
assert object_bytes is not None
|
||||
result: dict[str, Any] = {
|
||||
"path": str(path),
|
||||
"size": file_size,
|
||||
"magic": PACK_MAGIC.rstrip(b"\0").decode("ascii"),
|
||||
"version": PACK_VERSION,
|
||||
"header_bytes": PACK_HEADER.size,
|
||||
"entry_bytes": PACK_ENTRY.size,
|
||||
"index_count": expected_count,
|
||||
"data_start": data_start,
|
||||
"alignment": PACK_ALIGNMENT,
|
||||
"index_sha256": index_digest.hexdigest(),
|
||||
"physical_role_order": list(ROLE_ORDER),
|
||||
"active_moe_layer_ids": list(spec.active_moe_layer_ids),
|
||||
"num_experts": spec.num_experts,
|
||||
"top_k": spec.top_k,
|
||||
"object_bytes": object_bytes,
|
||||
"roles": role_summary,
|
||||
"payload_samples_verified": len(sampled_entries),
|
||||
"full_pack_hash": full_pack_hash,
|
||||
}
|
||||
if full_pack_hash:
|
||||
result["sha256"] = sha256_file(path)
|
||||
return result
|
||||
|
||||
|
||||
def _align_up(value: int, alignment: int = PACK_ALIGNMENT) -> int:
|
||||
return (value + alignment - 1) // alignment * alignment
|
||||
|
||||
|
||||
def _pack_layout(
|
||||
expert_tensors: dict[tuple[int, str], TensorRecord], spec: KimiK3Spec
|
||||
) -> tuple[list[tuple[PackEntryRecord, TensorRecord]], int, int]:
|
||||
index_count = len(spec.active_moe_layer_ids) * spec.num_experts * len(ROLE_ORDER)
|
||||
data_start = _align_up(PACK_HEADER.size + index_count * PACK_ENTRY.size)
|
||||
offset = data_start
|
||||
entries: list[tuple[PackEntryRecord, TensorRecord]] = []
|
||||
for layer in spec.active_moe_layer_ids:
|
||||
for expert in range(spec.num_experts):
|
||||
for role in ROLE_ORDER:
|
||||
tensor = expert_tensors[(layer, role)]
|
||||
expert_bytes = tensor.nbytes // spec.num_experts
|
||||
offset = _align_up(offset)
|
||||
entries.append(
|
||||
(
|
||||
PackEntryRecord(tensor.name, expert, offset, expert_bytes),
|
||||
tensor,
|
||||
)
|
||||
)
|
||||
offset += expert_bytes
|
||||
return entries, data_start, offset
|
||||
|
||||
|
||||
def estimate_ggml_moe_pack_size(
|
||||
expert_tensors: dict[tuple[int, str], TensorRecord], spec: KimiK3Spec
|
||||
) -> int:
|
||||
return _pack_layout(expert_tensors, spec)[2]
|
||||
|
||||
|
||||
def _copy_tensor_slice(
|
||||
source: BinaryIO, output: BinaryIO, offset: int, nbytes: int
|
||||
) -> None:
|
||||
source.seek(offset)
|
||||
remaining = nbytes
|
||||
while remaining:
|
||||
chunk = source.read(min(COPY_CHUNK_BYTES, remaining))
|
||||
if not chunk:
|
||||
raise EOFError(
|
||||
f"short GGUF read at offset {offset}; {remaining} bytes remain"
|
||||
)
|
||||
output.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
|
||||
|
||||
def write_ggml_moe_pack(
|
||||
path: Path,
|
||||
expert_tensors: dict[tuple[int, str], TensorRecord],
|
||||
spec: KimiK3Spec,
|
||||
*,
|
||||
progress: Callable[[int, int], None] | None = None,
|
||||
) -> int:
|
||||
entries, data_start, final_size = _pack_layout(expert_tensors, spec)
|
||||
path = path.resolve()
|
||||
partial = path.with_name(path.name + ".partial")
|
||||
if path.exists():
|
||||
raise FileExistsError(f"refusing to overwrite existing Expert Pack: {path}")
|
||||
if partial.exists():
|
||||
raise FileExistsError(f"partial Expert Pack already exists: {partial}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with ExitStack() as stack:
|
||||
sources = {
|
||||
source_path: stack.enter_context(Path(source_path).open("rb", buffering=0))
|
||||
for source_path in {tensor.shard_path for _, tensor in entries}
|
||||
}
|
||||
output = stack.enter_context(partial.open("xb", buffering=0))
|
||||
output.write(
|
||||
PACK_HEADER.pack(
|
||||
PACK_MAGIC,
|
||||
PACK_VERSION,
|
||||
PACK_HEADER.size,
|
||||
len(entries),
|
||||
data_start,
|
||||
)
|
||||
)
|
||||
for entry, _ in entries:
|
||||
encoded_name = entry.tensor_name.encode("utf-8")
|
||||
if len(encoded_name) >= 128:
|
||||
raise ValueError(
|
||||
f"expert tensor name is too long for the Pack index: {entry.tensor_name}"
|
||||
)
|
||||
output.write(
|
||||
PACK_ENTRY.pack(
|
||||
encoded_name.ljust(128, b"\0"),
|
||||
entry.expert,
|
||||
0,
|
||||
entry.offset,
|
||||
entry.nbytes,
|
||||
)
|
||||
)
|
||||
output.write(bytes(data_start - output.tell()))
|
||||
|
||||
total = len(entries)
|
||||
for index, (entry, tensor) in enumerate(entries, start=1):
|
||||
padding = entry.offset - output.tell()
|
||||
if padding < 0:
|
||||
raise RuntimeError("Expert Pack layout moved backwards")
|
||||
if padding:
|
||||
output.write(bytes(padding))
|
||||
source_offset = tensor.data_offset + entry.expert * (
|
||||
tensor.nbytes // spec.num_experts
|
||||
)
|
||||
_copy_tensor_slice(
|
||||
sources[tensor.shard_path], output, source_offset, entry.nbytes
|
||||
)
|
||||
if progress is not None and (index % 1024 == 0 or index == total):
|
||||
progress(index, total)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
|
||||
if partial.stat().st_size != final_size:
|
||||
raise RuntimeError(
|
||||
f"generated Expert Pack size {partial.stat().st_size} != {final_size}"
|
||||
)
|
||||
os.replace(partial, path)
|
||||
return final_size
|
||||
|
||||
|
||||
def create_manifest(
|
||||
*,
|
||||
gguf_dir: Path,
|
||||
expert_pack: Path,
|
||||
model_config: Path,
|
||||
tokenizer_dir: Path,
|
||||
payload_samples: int = 6,
|
||||
full_source_hashes: bool = False,
|
||||
full_pack_hash: bool = False,
|
||||
repo: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
config = json.loads(model_config.read_text(encoding="utf-8"))
|
||||
spec = KimiK3Spec.from_config(config)
|
||||
shard_paths = discover_gguf_shards(gguf_dir)
|
||||
shard_records, tensors, source_summary = scan_gguf_shards(
|
||||
shard_paths, full_source_hashes=full_source_hashes
|
||||
)
|
||||
expert_tensors = validate_expert_tensors(tensors, spec)
|
||||
pack = validate_ggml_moe_pack(
|
||||
expert_pack,
|
||||
expert_tensors,
|
||||
spec,
|
||||
payload_samples=payload_samples,
|
||||
full_pack_hash=full_pack_hash,
|
||||
)
|
||||
|
||||
tokenizer_files = []
|
||||
for path in sorted(tokenizer_dir.resolve(strict=True).iterdir()):
|
||||
if path.is_file():
|
||||
tokenizer_files.append(
|
||||
{
|
||||
"name": path.name,
|
||||
"size": path.stat().st_size,
|
||||
"sha256": sha256_file(path),
|
||||
}
|
||||
)
|
||||
tensor_records = [
|
||||
{
|
||||
"name": tensor.name,
|
||||
"shape": list(tensor.shape),
|
||||
"dtype": tensor.dtype,
|
||||
"dtype_id": tensor.dtype_id,
|
||||
"shard_index": tensor.shard_index,
|
||||
"data_offset": tensor.data_offset,
|
||||
"nbytes": tensor.nbytes,
|
||||
}
|
||||
for tensor in sorted(tensors, key=lambda item: item.name)
|
||||
]
|
||||
source_inventory = {
|
||||
"summary": source_summary,
|
||||
"shards": shard_records,
|
||||
"tensors": tensor_records,
|
||||
}
|
||||
model = {
|
||||
"config_path": str(model_config.resolve()),
|
||||
"config_sha256": sha256_file(model_config),
|
||||
"architecture": "KimiLinearForCausalLM",
|
||||
"num_hidden_layers": spec.num_hidden_layers,
|
||||
"active_moe_layer_ids": list(spec.active_moe_layer_ids),
|
||||
"num_experts": spec.num_experts,
|
||||
"num_experts_per_token": spec.top_k,
|
||||
"num_shared_experts": spec.num_shared_experts,
|
||||
"hidden_size": spec.hidden_size,
|
||||
"routed_expert_hidden_size": spec.routed_expert_hidden_size,
|
||||
"moe_intermediate_size": spec.moe_intermediate_size,
|
||||
"hidden_act": spec.hidden_act,
|
||||
}
|
||||
return {
|
||||
"complete": True,
|
||||
"format": FORMAT,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sglang_git_sha": _git_sha(repo),
|
||||
"hard_constraints": {
|
||||
"top_k": 16,
|
||||
"top_k_is_immutable": True,
|
||||
"all_selected_experts_must_execute": True,
|
||||
"expert_pruning_allowed": False,
|
||||
"requantization_allowed": False,
|
||||
},
|
||||
"model": model,
|
||||
"source": {
|
||||
**source_inventory,
|
||||
"inventory_sha256": canonical_sha256(source_inventory),
|
||||
},
|
||||
"expert_pack": pack,
|
||||
"tokenizer": {
|
||||
"path": str(tokenizer_dir.resolve()),
|
||||
"files": tokenizer_files,
|
||||
"inventory_sha256": canonical_sha256(tokenizer_files),
|
||||
},
|
||||
"verification": {
|
||||
"structure": "complete",
|
||||
"payload_samples": payload_samples,
|
||||
"full_source_hashes": full_source_hashes,
|
||||
"full_pack_hash": full_pack_hash,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""Validate or build the DeepSeek expert-pack used by the RTX 5090 benchmark."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from .format import read_header
|
||||
except ImportError:
|
||||
from format import read_header # type: ignore[no-redef]
|
||||
|
||||
|
||||
FORMAT = "SGLANG-EXPERTPACK-v1"
|
||||
EXPERT_PACK_FILENAME = "DeepSeek-V4-Flash.expert-pack"
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_json_atomic(path: Path, value: object) -> None:
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def normalize_manifest_identity(manifest_path: Path, manifest: dict) -> None:
|
||||
model = manifest["model"]
|
||||
normalized_model = {
|
||||
"ref": model["ref"],
|
||||
"model_identity_sha256": model["model_identity_sha256"],
|
||||
"config_sha256": model["config_sha256"],
|
||||
"num_layers": model["num_layers"],
|
||||
"num_routed_experts": model["num_routed_experts"],
|
||||
"top_k": model["top_k"],
|
||||
"single_gpu": model["single_gpu"],
|
||||
}
|
||||
if model != normalized_model:
|
||||
manifest["model"] = normalized_model
|
||||
write_json_atomic(manifest_path, manifest)
|
||||
|
||||
|
||||
def artifact_paths(source: Path) -> tuple[Path, Path, Path]:
|
||||
pack = source.parent / EXPERT_PACK_FILENAME
|
||||
manifest = source.parent / f"{EXPERT_PACK_FILENAME}.manifest.json"
|
||||
checkpoint = source.parent / f"{EXPERT_PACK_FILENAME}.checkpoint.json"
|
||||
return pack, manifest, checkpoint
|
||||
|
||||
|
||||
def validate_pack(
|
||||
pack: Path, manifest_path: Path, expected_source: Path | None = None
|
||||
) -> tuple[bool, str, dict | None]:
|
||||
try:
|
||||
pack = pack.resolve(strict=True)
|
||||
manifest_path = manifest_path.resolve(strict=True)
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
if manifest.get("format") != FORMAT or manifest.get("complete") is not True:
|
||||
raise ValueError(
|
||||
"manifest is not a completed SGLANG-EXPERTPACK-v1 manifest"
|
||||
)
|
||||
if Path(manifest["pack_path"]).resolve() != pack:
|
||||
raise ValueError(
|
||||
"manifest pack_path does not match the fixed expert-pack path"
|
||||
)
|
||||
if pack.stat().st_size != int(manifest["pack_size"]):
|
||||
raise ValueError("pack size does not match manifest")
|
||||
|
||||
source = Path(manifest["source"]["path"]).resolve(strict=True)
|
||||
if expected_source is not None and source != expected_source.resolve(
|
||||
strict=True
|
||||
):
|
||||
raise ValueError("manifest source path does not match --gguf")
|
||||
if source.stat().st_size != int(manifest["source"]["size"]):
|
||||
raise ValueError("source GGUF size does not match manifest")
|
||||
|
||||
with pack.open("rb", buffering=0) as stream:
|
||||
header = read_header(stream)
|
||||
stream.seek(header.header_bytes)
|
||||
raw_index = stream.read(header.index_count * header.entry_bytes)
|
||||
if len(raw_index) != header.index_count * header.entry_bytes:
|
||||
raise ValueError("pack index is truncated")
|
||||
if hashlib.sha256(raw_index).hexdigest() != manifest["index_sha256"]:
|
||||
raise ValueError("pack index SHA-256 does not match manifest")
|
||||
|
||||
model = manifest["model"]
|
||||
model_identity_sha256 = model.get(
|
||||
"model_identity_sha256", header.model_identity_sha256
|
||||
)
|
||||
model["model_identity_sha256"] = model_identity_sha256
|
||||
expected = (
|
||||
(header.index_count, manifest["index_count"], "index count"),
|
||||
(header.data_start, manifest["data_start"], "data start"),
|
||||
(header.num_layers, model["num_layers"], "layer count"),
|
||||
(header.num_experts, model["num_routed_experts"], "expert count"),
|
||||
(header.top_k, model["top_k"], "top-k"),
|
||||
(
|
||||
header.source_blob_sha256,
|
||||
manifest["source"]["sha256"],
|
||||
"source digest",
|
||||
),
|
||||
(
|
||||
header.model_identity_sha256,
|
||||
model_identity_sha256,
|
||||
"model identity digest",
|
||||
),
|
||||
(header.config_sha256, model["config_sha256"], "config digest"),
|
||||
)
|
||||
for actual, wanted, label in expected:
|
||||
if actual != wanted:
|
||||
raise ValueError(f"pack header {label} does not match manifest")
|
||||
return True, "header, index, source path and sizes are valid", manifest
|
||||
except Exception as exc:
|
||||
return False, str(exc), None
|
||||
|
||||
|
||||
def load_build_inputs(args: argparse.Namespace) -> dict[str, object]:
|
||||
source = args.gguf.resolve(strict=True)
|
||||
expert_pack, expert_pack_manifest, checkpoint = artifact_paths(source)
|
||||
model_config_path = args.model_config.resolve(strict=True)
|
||||
model_config = json.loads(model_config_path.read_text(encoding="utf-8"))
|
||||
source_sha256 = sha256_file(source)
|
||||
config_sha256 = sha256_file(model_config_path)
|
||||
model_identity = hashlib.sha256(
|
||||
f"sglang-deepseek-expert-pack-v1:{source_sha256}:{config_sha256}".encode(
|
||||
"ascii"
|
||||
)
|
||||
).hexdigest()
|
||||
return {
|
||||
"source": source,
|
||||
"source_sha256": source_sha256,
|
||||
"expert_pack": expert_pack,
|
||||
"expert_pack_manifest": expert_pack_manifest,
|
||||
"checkpoint": checkpoint,
|
||||
"config_blob": model_config_path,
|
||||
"config_sha256": config_sha256,
|
||||
"model_identity_sha256": model_identity,
|
||||
"num_layers": int(model_config["num_hidden_layers"]),
|
||||
"num_experts": int(model_config["n_routed_experts"]),
|
||||
"top_k": int(model_config["num_experts_per_tok"]),
|
||||
}
|
||||
|
||||
|
||||
def remove_invalid_outputs(pack: Path, manifest: Path, checkpoint: Path) -> None:
|
||||
for path in (pack, manifest, pack.with_name(pack.name + ".partial"), checkpoint):
|
||||
if path.exists():
|
||||
print(f"EXPERT_PACK_REMOVE_INVALID path={path}", flush=True)
|
||||
path.unlink()
|
||||
|
||||
|
||||
def build_pack(args: argparse.Namespace, inputs: dict[str, object]) -> None:
|
||||
build_script = Path(__file__).with_name("build.py")
|
||||
expert_pack = Path(inputs["expert_pack"])
|
||||
manifest = Path(inputs["expert_pack_manifest"])
|
||||
checkpoint = Path(inputs["checkpoint"])
|
||||
partial = expert_pack.with_name(expert_pack.name + ".partial")
|
||||
resume = partial.is_file() and checkpoint.is_file() and not expert_pack.exists()
|
||||
if not resume:
|
||||
remove_invalid_outputs(expert_pack, manifest, checkpoint)
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
str(build_script),
|
||||
"--source",
|
||||
str(inputs["source"]),
|
||||
"--source-sha256",
|
||||
str(inputs["source_sha256"]),
|
||||
"--output",
|
||||
str(expert_pack),
|
||||
"--manifest",
|
||||
str(manifest),
|
||||
"--checkpoint",
|
||||
str(checkpoint),
|
||||
"--model-ref",
|
||||
args.model_ref,
|
||||
"--model-identity-sha256",
|
||||
str(inputs["model_identity_sha256"]),
|
||||
"--config-blob",
|
||||
str(inputs["config_blob"]),
|
||||
"--config-sha256",
|
||||
str(inputs["config_sha256"]),
|
||||
"--num-layers",
|
||||
str(inputs["num_layers"]),
|
||||
"--num-experts",
|
||||
str(inputs["num_experts"]),
|
||||
"--top-k",
|
||||
str(inputs["top_k"]),
|
||||
"--alignment",
|
||||
str(args.alignment),
|
||||
"--safety-margin-gib",
|
||||
str(args.safety_margin_gib),
|
||||
]
|
||||
if args.inventory and args.inventory.is_file():
|
||||
command.extend(("--inventory", str(args.inventory.resolve())))
|
||||
if resume:
|
||||
command.append("--resume")
|
||||
print(
|
||||
f"EXPERT_PACK_BUILD_START output={expert_pack} resume={str(resume).lower()}",
|
||||
flush=True,
|
||||
)
|
||||
subprocess.run(command, check=True)
|
||||
print(f"EXPERT_PACK_BUILD_COMPLETE output={expert_pack}", flush=True)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--gguf", type=Path, required=True)
|
||||
parser.add_argument("--model-config", type=Path, required=True)
|
||||
parser.add_argument("--inventory", type=Path)
|
||||
parser.add_argument("--model-ref", default="deepseek-v4-flash")
|
||||
parser.add_argument("--alignment", type=int, default=4096)
|
||||
parser.add_argument("--safety-margin-gib", type=float, default=16.0)
|
||||
parser.add_argument("--check-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
source = args.gguf.resolve(strict=True)
|
||||
expert_pack, expert_pack_manifest, _ = artifact_paths(source)
|
||||
valid, reason, manifest = validate_pack(expert_pack, expert_pack_manifest, source)
|
||||
if valid and manifest is not None:
|
||||
normalize_manifest_identity(expert_pack_manifest, manifest)
|
||||
print(f"EXPERT_PACK_VALID path={expert_pack} detail={reason}", flush=True)
|
||||
return 0
|
||||
print(f"EXPERT_PACK_INVALID path={expert_pack} detail={reason}", flush=True)
|
||||
if args.check_only:
|
||||
return 1
|
||||
|
||||
existing_valid, existing_reason, existing_manifest = validate_pack(
|
||||
expert_pack, expert_pack_manifest
|
||||
)
|
||||
if existing_valid and existing_manifest is not None:
|
||||
existing_source = Path(existing_manifest["source"]["path"])
|
||||
raise RuntimeError(
|
||||
f"the fixed expert-pack already belongs to a different GGUF: {existing_source}; "
|
||||
f"move the requested GGUF to its own directory instead of overwriting {expert_pack}"
|
||||
)
|
||||
|
||||
inputs = load_build_inputs(args)
|
||||
build_pack(args, inputs)
|
||||
valid, reason, manifest = validate_pack(expert_pack, expert_pack_manifest, source)
|
||||
if not valid or manifest is None:
|
||||
raise RuntimeError(f"generated expert-pack failed validation: {reason}")
|
||||
normalize_manifest_identity(expert_pack_manifest, manifest)
|
||||
print(f"EXPERT_PACK_READY path={expert_pack} detail={reason}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Prepare a small, zero-copy Kimi K3 GGUF/expert-pack adapter manifest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from .kimi_ggml import create_manifest, write_json_atomic
|
||||
except ImportError:
|
||||
from kimi_ggml import create_manifest, write_json_atomic # type: ignore[no-redef]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--gguf-dir", type=Path, required=True)
|
||||
parser.add_argument("--expert-pack", type=Path, required=True)
|
||||
parser.add_argument("--model-config", type=Path, required=True)
|
||||
parser.add_argument("--tokenizer-dir", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--payload-samples",
|
||||
type=int,
|
||||
default=6,
|
||||
help="Evenly spaced pack entries compared byte-for-byte with GGUF (default: 6).",
|
||||
)
|
||||
parser.add_argument("--full-source-hashes", action="store_true")
|
||||
parser.add_argument("--full-pack-hash", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.payload_samples < 0:
|
||||
raise ValueError("--payload-samples must be non-negative")
|
||||
repo = Path(__file__).resolve().parent
|
||||
manifest = create_manifest(
|
||||
gguf_dir=args.gguf_dir,
|
||||
expert_pack=args.expert_pack,
|
||||
model_config=args.model_config.resolve(strict=True),
|
||||
tokenizer_dir=args.tokenizer_dir,
|
||||
payload_samples=args.payload_samples,
|
||||
full_source_hashes=args.full_source_hashes,
|
||||
full_pack_hash=args.full_pack_hash,
|
||||
repo=repo,
|
||||
)
|
||||
write_json_atomic(args.output.resolve(), manifest)
|
||||
summary = {
|
||||
"manifest": str(args.output.resolve()),
|
||||
"format": manifest["format"],
|
||||
"source_shards": manifest["source"]["summary"]["shard_count"],
|
||||
"source_tensors": manifest["source"]["summary"]["tensor_count"],
|
||||
"pack_entries": manifest["expert_pack"]["index_count"],
|
||||
"pack_index_sha256": manifest["expert_pack"]["index_sha256"],
|
||||
"top_k": manifest["hard_constraints"]["top_k"],
|
||||
"payload_samples_verified": manifest["expert_pack"]["payload_samples_verified"],
|
||||
}
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Validate or build the Kimi K3 expert pack derived from GGUF shards."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from .kimi_ggml import (
|
||||
SHARD_RE,
|
||||
KimiK3Spec,
|
||||
discover_gguf_shards,
|
||||
estimate_ggml_moe_pack_size,
|
||||
scan_gguf_shards,
|
||||
validate_expert_tensors,
|
||||
validate_ggml_moe_pack,
|
||||
write_ggml_moe_pack,
|
||||
)
|
||||
except ImportError:
|
||||
from kimi_ggml import ( # type: ignore[no-redef]
|
||||
SHARD_RE,
|
||||
KimiK3Spec,
|
||||
discover_gguf_shards,
|
||||
estimate_ggml_moe_pack_size,
|
||||
scan_gguf_shards,
|
||||
validate_expert_tensors,
|
||||
validate_ggml_moe_pack,
|
||||
write_ggml_moe_pack,
|
||||
)
|
||||
|
||||
|
||||
def expert_pack_path(gguf: Path) -> Path:
|
||||
match = SHARD_RE.search(gguf.name)
|
||||
if match is None:
|
||||
raise ValueError(f"Kimi GGUF name is not a numbered shard: {gguf}")
|
||||
return gguf.parent / f"{gguf.name[: match.start()]}.expert-major.pack"
|
||||
|
||||
|
||||
def validate_pack(pack: Path, expert_tensors: dict, spec: KimiK3Spec) -> str:
|
||||
result = validate_ggml_moe_pack(
|
||||
pack, expert_tensors, spec, payload_samples=6, full_pack_hash=False
|
||||
)
|
||||
return (
|
||||
f"entries={result['index_count']} size={result['size']} "
|
||||
f"samples={result['payload_samples_verified']}"
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--gguf", type=Path, required=True)
|
||||
parser.add_argument("--model-config", type=Path, required=True)
|
||||
parser.add_argument("--safety-margin-gib", type=float, default=2.0)
|
||||
parser.add_argument("--check-only", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
gguf = args.gguf.expanduser().resolve(strict=True)
|
||||
model_config = args.model_config.expanduser().resolve(strict=True)
|
||||
if args.safety_margin_gib < 0:
|
||||
raise ValueError("--safety-margin-gib must be non-negative")
|
||||
|
||||
shards = discover_gguf_shards(gguf.parent)
|
||||
if gguf not in shards:
|
||||
raise ValueError(f"--gguf is not part of the discovered shard set: {gguf}")
|
||||
config = json.loads(model_config.read_text(encoding="utf-8"))
|
||||
spec = KimiK3Spec.from_config(config)
|
||||
_, tensors, _ = scan_gguf_shards(shards)
|
||||
expert_tensors = validate_expert_tensors(tensors, spec)
|
||||
pack = expert_pack_path(gguf)
|
||||
partial = pack.with_name(pack.name + ".partial")
|
||||
lock_path = pack.with_name(pack.name + ".lock")
|
||||
|
||||
with lock_path.open("w") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
detail = validate_pack(pack, expert_tensors, spec)
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"EXPERT_PACK_INVALID path={pack} detail={exc}", flush=True)
|
||||
if args.check_only:
|
||||
return 1
|
||||
else:
|
||||
print(f"EXPERT_PACK_VALID path={pack} {detail}", flush=True)
|
||||
return 0
|
||||
|
||||
for path in (pack, partial):
|
||||
if path.exists():
|
||||
print(f"EXPERT_PACK_REMOVE_INVALID path={path}", flush=True)
|
||||
path.unlink()
|
||||
|
||||
estimated_size = estimate_ggml_moe_pack_size(expert_tensors, spec)
|
||||
safety_margin = int(args.safety_margin_gib * 1024**3)
|
||||
available = shutil.disk_usage(pack.parent).free
|
||||
if available < estimated_size + safety_margin:
|
||||
raise OSError(
|
||||
f"insufficient space for Kimi Expert Pack: available={available}, "
|
||||
f"required={estimated_size + safety_margin}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"EXPERT_PACK_BUILD_START output={pack} size={estimated_size}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def report_progress(completed: int, total: int) -> None:
|
||||
print(
|
||||
f"EXPERT_PACK_BUILD_PROGRESS completed={completed} total={total}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
write_ggml_moe_pack(pack, expert_tensors, spec, progress=report_progress)
|
||||
detail = validate_pack(pack, expert_tensors, spec)
|
||||
print(f"EXPERT_PACK_READY path={pack} {detail}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from .format import (
|
||||
ROLE_NAMES,
|
||||
IndexEntry,
|
||||
read_header,
|
||||
read_index,
|
||||
sha256_file,
|
||||
)
|
||||
except ImportError:
|
||||
from format import ( # type: ignore[no-redef]
|
||||
ROLE_NAMES,
|
||||
IndexEntry,
|
||||
read_header,
|
||||
read_index,
|
||||
sha256_file,
|
||||
)
|
||||
|
||||
|
||||
CHUNK_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
def hash_range(stream, offset: int, nbytes: int) -> str:
|
||||
digest = hashlib.sha256()
|
||||
stream.seek(offset)
|
||||
remaining = nbytes
|
||||
while remaining:
|
||||
chunk = stream.read(min(remaining, CHUNK_BYTES))
|
||||
if not chunk:
|
||||
raise EOFError(f"short read at offset {offset}; {remaining} bytes remain")
|
||||
digest.update(chunk)
|
||||
remaining -= len(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def compare_ranges(source, pack, entry: IndexEntry) -> None:
|
||||
remaining = entry.pack_nbytes
|
||||
source_offset = entry.source_slice_offset
|
||||
pack_offset = entry.pack_offset
|
||||
while remaining:
|
||||
length = min(remaining, CHUNK_BYTES)
|
||||
source.seek(source_offset)
|
||||
pack.seek(pack_offset)
|
||||
source_data = source.read(length)
|
||||
pack_data = pack.read(length)
|
||||
if len(source_data) != length or len(pack_data) != length:
|
||||
raise EOFError(f"short source/pack read for entry {entry.key}")
|
||||
if source_data != pack_data:
|
||||
raise ValueError(f"source/pack bytes differ for entry {entry.key}")
|
||||
source_offset += length
|
||||
pack_offset += length
|
||||
remaining -= length
|
||||
|
||||
|
||||
def validate(args: argparse.Namespace) -> dict[str, object]:
|
||||
started = time.monotonic()
|
||||
pack_path = args.pack.resolve(strict=True)
|
||||
manifest_path = args.manifest.resolve(strict=True)
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
if manifest.get("format") != "SGLANG-EXPERTPACK-v1" or not manifest.get("complete"):
|
||||
raise ValueError("manifest is not a complete SGLANG-EXPERTPACK-v1 manifest")
|
||||
if Path(manifest["pack_path"]).resolve() != pack_path:
|
||||
raise ValueError("manifest pack path does not match --pack")
|
||||
if pack_path.stat().st_size != int(manifest["pack_size"]):
|
||||
raise ValueError("pack size does not match manifest")
|
||||
|
||||
source_path = (
|
||||
args.source.resolve(strict=True)
|
||||
if args.source is not None
|
||||
else Path(manifest["source"]["path"]).resolve(strict=True)
|
||||
)
|
||||
if source_path.stat().st_size != int(manifest["source"]["size"]):
|
||||
raise ValueError("source size does not match manifest")
|
||||
|
||||
with pack_path.open("rb", buffering=0) as pack:
|
||||
header = read_header(pack)
|
||||
index_start = header.header_bytes
|
||||
pack.seek(index_start)
|
||||
raw_index = pack.read(header.index_count * header.entry_bytes)
|
||||
if len(raw_index) != header.index_count * header.entry_bytes:
|
||||
raise ValueError("pack index is truncated")
|
||||
if hashlib.sha256(raw_index).hexdigest() != manifest["index_sha256"]:
|
||||
raise ValueError("pack index SHA-256 does not match manifest")
|
||||
entries = read_index(pack, header)
|
||||
|
||||
model = manifest["model"]
|
||||
source = manifest["source"]
|
||||
for actual, expected, field in (
|
||||
(
|
||||
header.model_identity_sha256,
|
||||
model["model_identity_sha256"],
|
||||
"model identity digest",
|
||||
),
|
||||
(header.source_blob_sha256, source["sha256"], "source digest"),
|
||||
(header.config_sha256, model["config_sha256"], "config digest"),
|
||||
(header.num_layers, model["num_layers"], "layer count"),
|
||||
(header.num_experts, model["num_routed_experts"], "expert count"),
|
||||
(header.top_k, model["top_k"], "top-k"),
|
||||
(header.index_count, manifest["index_count"], "index count"),
|
||||
(header.data_start, manifest["data_start"], "data start"),
|
||||
(header.alignment, manifest["alignment"], "alignment"),
|
||||
):
|
||||
if actual != expected:
|
||||
raise ValueError(f"pack header {field} does not match manifest")
|
||||
|
||||
expected_keys = {
|
||||
(layer, expert, role)
|
||||
for layer in range(header.num_layers)
|
||||
for expert in range(header.num_experts)
|
||||
for role in ROLE_NAMES
|
||||
}
|
||||
by_key = {(entry.layer, entry.expert, entry.role): entry for entry in entries}
|
||||
if len(by_key) != len(entries) or set(by_key) != expected_keys:
|
||||
raise ValueError("pack index does not have exact layer/expert/role coverage")
|
||||
|
||||
tensor_map = {tensor["name"]: tensor for tensor in manifest["tensors"]}
|
||||
non_routed = [
|
||||
tensor for tensor in manifest["tensors"] if tensor["category"] == "non_routed"
|
||||
]
|
||||
routed = [
|
||||
tensor
|
||||
for tensor in manifest["tensors"]
|
||||
if tensor["category"] == "routed_expert"
|
||||
]
|
||||
if len(non_routed) != manifest["coverage"]["non_routed_tensor_count"]:
|
||||
raise ValueError("non-routed tensor coverage does not match manifest summary")
|
||||
if len(routed) != manifest["coverage"]["routed_tensor_count"]:
|
||||
raise ValueError("routed tensor coverage does not match manifest summary")
|
||||
|
||||
ranges = []
|
||||
object_stride = int(manifest["object_stride"])
|
||||
for layer in range(header.num_layers):
|
||||
for expert in range(header.num_experts):
|
||||
object_entries = [by_key[(layer, expert, role)] for role in ROLE_NAMES]
|
||||
expected_object_start = (
|
||||
header.data_start
|
||||
+ (layer * header.num_experts + expert) * object_stride
|
||||
)
|
||||
if object_entries[0].pack_offset != expected_object_start:
|
||||
raise ValueError(
|
||||
f"object {(layer, expert)} is not at its expected aligned offset"
|
||||
)
|
||||
if expected_object_start % header.alignment:
|
||||
raise ValueError(f"object {(layer, expert)} is not aligned")
|
||||
cursor = expected_object_start
|
||||
generations = set()
|
||||
for entry in object_entries:
|
||||
entry.pack()
|
||||
tensor = tensor_map.get(entry.tensor_name)
|
||||
if tensor is None or tensor["category"] != "routed_expert":
|
||||
raise ValueError(
|
||||
f"entry {entry.key} does not map to a routed tensor"
|
||||
)
|
||||
if (
|
||||
entry.pack_offset != cursor
|
||||
or entry.pack_nbytes != entry.source_slice_nbytes
|
||||
):
|
||||
raise ValueError(
|
||||
f"entry {entry.key} breaks identity triplet layout"
|
||||
)
|
||||
if (
|
||||
entry.transform_id != "identity-v1"
|
||||
or entry.checksum != entry.source_slice_sha256
|
||||
):
|
||||
raise ValueError(
|
||||
f"entry {entry.key} is not an auditable identity transform"
|
||||
)
|
||||
if entry.source_tensor_offset != tensor["source_offset"]:
|
||||
raise ValueError(f"entry {entry.key} source tensor offset mismatch")
|
||||
if entry.source_tensor_nbytes != tensor["source_nbytes"]:
|
||||
raise ValueError(f"entry {entry.key} source tensor size mismatch")
|
||||
if entry.source_tensor_sha256 != tensor["source_payload_sha256"]:
|
||||
raise ValueError(f"entry {entry.key} source tensor hash mismatch")
|
||||
expected_slice_offset = (
|
||||
entry.source_tensor_offset + expert * entry.source_slice_nbytes
|
||||
)
|
||||
if entry.source_slice_offset != expected_slice_offset:
|
||||
raise ValueError(f"entry {entry.key} source slice offset mismatch")
|
||||
if entry.source_slice_offset + entry.source_slice_nbytes > (
|
||||
entry.source_tensor_offset + entry.source_tensor_nbytes
|
||||
):
|
||||
raise ValueError(f"entry {entry.key} source slice is out of bounds")
|
||||
ranges.append(
|
||||
(
|
||||
entry.pack_offset,
|
||||
entry.pack_offset + entry.pack_nbytes,
|
||||
entry.key,
|
||||
)
|
||||
)
|
||||
generations.add(entry.generation)
|
||||
cursor += entry.pack_nbytes
|
||||
if len(generations) != 1:
|
||||
raise ValueError(
|
||||
f"object {(layer, expert)} has inconsistent generations"
|
||||
)
|
||||
if cursor > expected_object_start + object_stride:
|
||||
raise ValueError(f"object {(layer, expert)} exceeds its stride")
|
||||
|
||||
ranges.sort()
|
||||
previous_end = header.data_start
|
||||
for start, end, key in ranges:
|
||||
if start < previous_end or end > pack_path.stat().st_size:
|
||||
raise ValueError(f"overlapping or out-of-range pack entry {key}")
|
||||
previous_end = end
|
||||
|
||||
bytes_hashed = 0
|
||||
pack_hash_ok = None
|
||||
if args.full_pack_hash:
|
||||
pack_hash_ok = sha256_file(pack_path) == manifest["pack_sha256"]
|
||||
bytes_hashed += pack_path.stat().st_size
|
||||
if not pack_hash_ok:
|
||||
raise ValueError("full pack SHA-256 does not match manifest")
|
||||
|
||||
entry_hash_count = 0
|
||||
if args.full_pack_entry_hashes:
|
||||
with pack_path.open("rb", buffering=0) as pack:
|
||||
for entry in sorted(entries, key=lambda value: value.pack_offset):
|
||||
if (
|
||||
hash_range(pack, entry.pack_offset, entry.pack_nbytes)
|
||||
!= entry.checksum
|
||||
):
|
||||
raise ValueError(
|
||||
f"pack payload checksum mismatch for entry {entry.key}"
|
||||
)
|
||||
bytes_hashed += entry.pack_nbytes
|
||||
entry_hash_count += 1
|
||||
|
||||
source_tensor_hash_count = 0
|
||||
if args.full_source_tensor_hashes:
|
||||
with source_path.open("rb", buffering=0) as source_stream:
|
||||
for tensor in sorted(
|
||||
manifest["tensors"], key=lambda value: value["source_offset"]
|
||||
):
|
||||
digest = hash_range(
|
||||
source_stream,
|
||||
int(tensor["source_offset"]),
|
||||
int(tensor["source_nbytes"]),
|
||||
)
|
||||
if digest != tensor["source_payload_sha256"]:
|
||||
raise ValueError(
|
||||
f"source tensor hash mismatch for {tensor['name']}"
|
||||
)
|
||||
bytes_hashed += int(tensor["source_nbytes"])
|
||||
source_tensor_hash_count += 1
|
||||
|
||||
source_file_hash_ok = None
|
||||
if args.full_source_file_hash:
|
||||
source_file_hash_ok = sha256_file(source_path) == source["sha256"]
|
||||
bytes_hashed += source_path.stat().st_size
|
||||
if not source_file_hash_ok:
|
||||
raise ValueError("full source file SHA-256 does not match manifest")
|
||||
|
||||
sample_count = min(args.source_range_samples, len(entries))
|
||||
sampled_entries = []
|
||||
if sample_count:
|
||||
seed = int(source["sha256"][:16], 16)
|
||||
sampled_entries = random.Random(seed).sample(entries, sample_count)
|
||||
with (
|
||||
source_path.open("rb", buffering=0) as source_stream,
|
||||
pack_path.open("rb", buffering=0) as pack_stream,
|
||||
):
|
||||
for entry in sampled_entries:
|
||||
compare_ranges(source_stream, pack_stream, entry)
|
||||
bytes_hashed += entry.pack_nbytes * 2
|
||||
|
||||
elapsed_s = time.monotonic() - started
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"pack": str(pack_path),
|
||||
"manifest": str(manifest_path),
|
||||
"source": str(source_path),
|
||||
"layers": header.num_layers,
|
||||
"experts_per_layer": header.num_experts,
|
||||
"top_k": header.top_k,
|
||||
"index_count": len(entries),
|
||||
"object_count": header.num_layers * header.num_experts,
|
||||
"non_routed_tensor_count": len(non_routed),
|
||||
"routed_tensor_count": len(routed),
|
||||
"full_pack_hash": pack_hash_ok,
|
||||
"full_pack_entry_hash_count": entry_hash_count,
|
||||
"full_source_tensor_hash_count": source_tensor_hash_count,
|
||||
"full_source_file_hash": source_file_hash_ok,
|
||||
"source_range_compare_count": len(sampled_entries),
|
||||
"bytes_verified": bytes_hashed,
|
||||
"elapsed_s": elapsed_s,
|
||||
"verified_mib_s": bytes_hashed / 1024**2 / elapsed_s if bytes_hashed else None,
|
||||
}
|
||||
if args.report is not None:
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate SGLANG-EXPERTPACK-v1")
|
||||
parser.add_argument("--pack", type=Path, required=True)
|
||||
parser.add_argument("--manifest", type=Path, required=True)
|
||||
parser.add_argument("--source", type=Path)
|
||||
parser.add_argument("--source-range-samples", type=int, default=96)
|
||||
parser.add_argument("--full-pack-hash", action="store_true")
|
||||
parser.add_argument("--full-pack-entry-hashes", action="store_true")
|
||||
parser.add_argument("--full-source-tensor-hashes", action="store_true")
|
||||
parser.add_argument("--full-source-file-hash", action="store_true")
|
||||
parser.add_argument("--full", action="store_true")
|
||||
parser.add_argument("--report", type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.source_range_samples < 0:
|
||||
parser.error("--source-range-samples must be non-negative")
|
||||
if args.full:
|
||||
args.full_pack_hash = True
|
||||
args.full_pack_entry_hashes = True
|
||||
args.full_source_tensor_hashes = True
|
||||
args.full_source_file_hash = True
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
print(json.dumps(validate(args), indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -114,14 +114,19 @@ def _expert_pack_path(gguf: Path) -> Path:
|
||||
return gguf.parent / f"{gguf.name[: match.start()]}.expert-major.pack"
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
for candidate in Path(__file__).resolve().parents:
|
||||
if (candidate / "tools" / "expert_pack" / "prepare_kimi_pack.py").is_file():
|
||||
return candidate
|
||||
raise RuntimeError(
|
||||
"expert_pack cannot auto-build Kimi artifacts from an installed package; "
|
||||
"run from an SGLang source checkout"
|
||||
def _expert_pack_tools_dir() -> Path:
|
||||
tools_dir = Path(__file__).with_name("expert_pack")
|
||||
required_tools = (
|
||||
"prepare_deepseek_pack.py",
|
||||
"prepare_kimi_manifest.py",
|
||||
"prepare_kimi_pack.py",
|
||||
)
|
||||
missing = [name for name in required_tools if not (tools_dir / name).is_file()]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
"expert_pack preparation tools are missing: " + ", ".join(missing)
|
||||
)
|
||||
return tools_dir
|
||||
|
||||
|
||||
def ensure_kimi_assets(
|
||||
@@ -144,26 +149,26 @@ def ensure_kimi_assets(
|
||||
manifest = artifact_dir / "kimi-k3-expert-pack.manifest.json"
|
||||
tokenizer = resolve_kimi_tokenizer(gguf, tokenizer_dir)
|
||||
lock_path = pack.with_name(pack.name + ".startup.lock")
|
||||
repo = _repo_root()
|
||||
tools_dir = _expert_pack_tools_dir()
|
||||
with lock_path.open("w") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
model_dir = prepare_kimi_model_metadata(tokenizer, artifact_dir)
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(repo / "tools" / "expert_pack" / "prepare_kimi_pack.py"),
|
||||
str(tools_dir / "prepare_kimi_pack.py"),
|
||||
"--gguf",
|
||||
str(gguf),
|
||||
"--model-config",
|
||||
str(model_dir / "config.json"),
|
||||
],
|
||||
cwd=repo,
|
||||
cwd=tools_dir,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(repo / "tools" / "expert_pack" / "prepare_kimi_manifest.py"),
|
||||
str(tools_dir / "prepare_kimi_manifest.py"),
|
||||
"--gguf-dir",
|
||||
str(gguf_dir),
|
||||
"--expert-pack",
|
||||
@@ -177,7 +182,7 @@ def ensure_kimi_assets(
|
||||
"--payload-samples",
|
||||
"6",
|
||||
],
|
||||
cwd=repo,
|
||||
cwd=tools_dir,
|
||||
check=True,
|
||||
)
|
||||
return {
|
||||
@@ -480,9 +485,9 @@ def _deepseek_digest(value: object, field: str) -> str:
|
||||
|
||||
|
||||
def _prepare_deepseek_pack(
|
||||
source: Path, model_config: Path, repo: Path
|
||||
source: Path, model_config: Path, tools_dir: Path
|
||||
) -> tuple[Path, Path]:
|
||||
tool = repo / "tools" / "expert_pack" / "prepare_deepseek_pack.py"
|
||||
tool = tools_dir / "prepare_deepseek_pack.py"
|
||||
if not tool.is_file():
|
||||
raise FileNotFoundError(f"missing DeepSeek Expert Pack preparer: {tool}")
|
||||
subprocess.run(
|
||||
@@ -494,7 +499,7 @@ def _prepare_deepseek_pack(
|
||||
"--model-config",
|
||||
str(model_config),
|
||||
],
|
||||
cwd=repo,
|
||||
cwd=tools_dir,
|
||||
check=True,
|
||||
)
|
||||
return (
|
||||
@@ -514,14 +519,14 @@ def prepare_raw_deepseek_server_args(
|
||||
source = Path(cfg.model_path).expanduser().resolve(strict=True)
|
||||
if not source.is_file():
|
||||
return
|
||||
repo = _repo_root()
|
||||
tools_dir = _expert_pack_tools_dir()
|
||||
artifact_dir = _deepseek_artifact_dir_for_source(source).resolve()
|
||||
lock_path = artifact_dir / "deepseek-v4-startup.lock"
|
||||
artifact_dir.mkdir(parents=True, exist_ok=True)
|
||||
with lock_path.open("w") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
model_config = _prepare_deepseek_model_metadata(source, artifact_dir)
|
||||
pack, manifest = _prepare_deepseek_pack(source, model_config, repo)
|
||||
pack, manifest = _prepare_deepseek_pack(source, model_config, tools_dir)
|
||||
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
source_value = manifest_value.get("source") or {}
|
||||
model_value = manifest_value.get("model") or {}
|
||||
|
||||
Reference in New Issue
Block a user