mirror of
https://github.com/kvcache-ai/ktransformers.git
synced 2026-10-29 06:04:23 +08:00
357 lines
13 KiB
Python
357 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Freeze four public main heads and audit source-built release artifacts.
|
|
|
|
This module never uploads packages or changes package versions/dependencies.
|
|
Raw wheels remain non-publishable; release_stack.py gates final promotion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import email
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
from packaging.requirements import Requirement
|
|
from packaging.utils import canonicalize_name, parse_wheel_filename
|
|
|
|
REPOSITORIES = {
|
|
"ktransformers": "kvcache-ai/ktransformers",
|
|
"sglang": "kvcache-ai/sglang",
|
|
"transformers": "kvcache-ai/transformers",
|
|
"accelerate": "kvcache-ai/accelerate",
|
|
}
|
|
PACKAGE_SOURCES = {
|
|
"ktransformers": "ktransformers",
|
|
"kt-kernel": "ktransformers",
|
|
"sglang-kt": "sglang",
|
|
"sgl-kernel-kt": "sglang",
|
|
"transformers-kt": "transformers",
|
|
"accelerate-kt": "accelerate",
|
|
}
|
|
EXACT_DEPENDENCIES = {
|
|
"ktransformers": ("kt-kernel", "sglang-kt", "transformers-kt", "accelerate-kt"),
|
|
"sglang-kt": ("kt-kernel", "transformers-kt"),
|
|
}
|
|
FULL_SHA = re.compile(r"[0-9a-f]{40}\Z")
|
|
|
|
|
|
def run(*args: str, cwd: Path | None = None) -> str:
|
|
return subprocess.check_output(args, cwd=cwd, text=True).strip()
|
|
|
|
|
|
def save_json(path: Path, data: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
|
|
|
|
def resolve_main(repository: str) -> str:
|
|
output = run(
|
|
"git",
|
|
"ls-remote",
|
|
"--exit-code",
|
|
f"https://github.com/{repository}.git",
|
|
"refs/heads/main",
|
|
)
|
|
rows = [row.split() for row in output.splitlines() if row.strip()]
|
|
if len(rows) != 1 or len(rows[0]) != 2:
|
|
raise ValueError(f"Ambiguous main ref for {repository}: {output!r}")
|
|
sha, ref = rows[0]
|
|
if ref != "refs/heads/main" or not FULL_SHA.fullmatch(sha):
|
|
raise ValueError(f"Invalid main ref for {repository}: {output!r}")
|
|
return sha
|
|
|
|
|
|
def snapshot(workflow_sha: str, resolver=resolve_main, attempts: int = 3) -> dict:
|
|
if not FULL_SHA.fullmatch(workflow_sha):
|
|
raise ValueError("workflow_sha must be the full workflow commit SHA")
|
|
# GitHub has no atomic snapshot across repositories. Require two consecutive
|
|
# identical observations; if a merge races us, restart the entire snapshot.
|
|
for _ in range(attempts):
|
|
first = {key: resolver(repo) for key, repo in REPOSITORIES.items()}
|
|
second = {key: resolver(repo) for key, repo in REPOSITORIES.items()}
|
|
if first == second:
|
|
lock = {
|
|
"schema_version": 1,
|
|
"observed_at": datetime.now(timezone.utc).isoformat(),
|
|
"workflow_sha": workflow_sha,
|
|
"sources": {
|
|
key: {
|
|
"repository": repo,
|
|
"ref": "refs/heads/main",
|
|
"sha": first[key],
|
|
}
|
|
for key, repo in REPOSITORIES.items()
|
|
},
|
|
}
|
|
validate_lock(lock)
|
|
return lock
|
|
raise RuntimeError(
|
|
"main heads kept moving; retry the workflow after the merges settle"
|
|
)
|
|
|
|
|
|
def validate_lock(lock: dict) -> None:
|
|
if lock.get("schema_version") != 1:
|
|
raise ValueError("Unsupported source lock schema")
|
|
if not FULL_SHA.fullmatch(lock.get("workflow_sha", "")):
|
|
raise ValueError("Invalid workflow SHA")
|
|
if set(lock.get("sources", {})) != set(REPOSITORIES):
|
|
raise ValueError(
|
|
"The source lock must contain exactly the four KT repositories"
|
|
)
|
|
for key, repo in REPOSITORIES.items():
|
|
source = lock["sources"][key]
|
|
if source.get("repository") != repo or source.get("ref") != "refs/heads/main":
|
|
raise ValueError(f"Only {repo} main is an allowed source")
|
|
if not FULL_SHA.fullmatch(source.get("sha", "")):
|
|
raise ValueError(f"Invalid source SHA for {key}")
|
|
|
|
|
|
def read_lock(path: Path) -> dict:
|
|
lock = json.loads(path.read_text())
|
|
validate_lock(lock)
|
|
return lock
|
|
|
|
|
|
def checkout(lock: dict, destination: Path) -> None:
|
|
validate_lock(lock)
|
|
destination.mkdir(parents=True, exist_ok=False)
|
|
for key, source in lock["sources"].items():
|
|
target = destination / key
|
|
run("git", "init", str(target))
|
|
run(
|
|
"git",
|
|
"remote",
|
|
"add",
|
|
"origin",
|
|
f"https://github.com/{source['repository']}.git",
|
|
cwd=target,
|
|
)
|
|
run("git", "fetch", "--depth=1", "origin", source["sha"], cwd=target)
|
|
run("git", "checkout", "--detach", "FETCH_HEAD", cwd=target)
|
|
if run("git", "rev-parse", "HEAD", cwd=target) != source["sha"]:
|
|
raise RuntimeError(f"Checkout SHA mismatch for {key}")
|
|
modules = target / ".gitmodules"
|
|
if modules.exists():
|
|
entries = run(
|
|
"git",
|
|
"config",
|
|
"--file",
|
|
".gitmodules",
|
|
"--get-regexp",
|
|
r"^submodule\..*\.path$",
|
|
cwd=target,
|
|
)
|
|
paths = [line.split(maxsplit=1)[1] for line in entries.splitlines()]
|
|
# SGLang is checked out independently at the locked main SHA, never
|
|
# built from the potentially stale KT gitlink.
|
|
paths = [
|
|
p
|
|
for p in paths
|
|
if not (key == "ktransformers" and p == "third_party/sglang")
|
|
]
|
|
for path in paths:
|
|
if (
|
|
PurePosixPath(path).is_absolute()
|
|
or ".." in PurePosixPath(path).parts
|
|
):
|
|
raise ValueError(f"Unsafe submodule path: {path}")
|
|
if paths:
|
|
run(
|
|
"git",
|
|
"submodule",
|
|
"update",
|
|
"--init",
|
|
"--recursive",
|
|
"--",
|
|
*paths,
|
|
cwd=target,
|
|
)
|
|
|
|
|
|
def source_evidence(lock: dict, sources: Path) -> dict:
|
|
evidence = {}
|
|
for key, source in lock["sources"].items():
|
|
target = sources / key
|
|
sha = run("git", "rev-parse", "HEAD", cwd=target)
|
|
# Native builds can apply checked-in patches inside dependency
|
|
# submodules. Record their status separately, without accepting edits
|
|
# to the four repositories' own tracked files.
|
|
diff = run("git", "diff", "HEAD", "--ignore-submodules=dirty", cwd=target)
|
|
if sha != source["sha"] or diff:
|
|
raise RuntimeError(f"Source changed during the build: {key}")
|
|
evidence[key] = {
|
|
**source,
|
|
"submodules": run(
|
|
"git", "submodule", "status", "--recursive", cwd=target
|
|
).splitlines(),
|
|
}
|
|
return evidence
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
with path.open("rb") as stream:
|
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
|
|
|
|
def inspect_wheel(path: Path) -> dict:
|
|
name, version, _, tags = parse_wheel_filename(path.name)
|
|
with zipfile.ZipFile(path) as wheel:
|
|
metadata_files = [
|
|
p
|
|
for p in wheel.namelist()
|
|
if p.endswith(".dist-info/METADATA") and len(PurePosixPath(p).parts) == 2
|
|
]
|
|
if len(metadata_files) != 1:
|
|
raise ValueError(f"Expected one METADATA in {path.name}")
|
|
metadata = email.message_from_bytes(wheel.read(metadata_files[0]))
|
|
if canonicalize_name(metadata["Name"]) != name or metadata["Version"] != str(
|
|
version
|
|
):
|
|
raise ValueError(f"Wheel filename and METADATA disagree: {path.name}")
|
|
return {
|
|
"name": name,
|
|
"version": str(version),
|
|
"filename": path.name,
|
|
"sha256": sha256(path),
|
|
"size": path.stat().st_size,
|
|
"tags": sorted(str(tag) for tag in tags),
|
|
"requires_dist": metadata.get_all("Requires-Dist", []),
|
|
}
|
|
|
|
|
|
def active_requirements(wheel: dict) -> list[Requirement]:
|
|
# Test/dev extras are intentionally outside the published user workflows.
|
|
result = []
|
|
for text in wheel["requires_dist"]:
|
|
req = Requirement(text)
|
|
if req.marker is None or any(
|
|
req.marker.evaluate({"extra": extra}) for extra in ("", "sglang", "sft")
|
|
):
|
|
result.append(req)
|
|
return result
|
|
|
|
|
|
def audit_wheels(wheels: list[dict], *, python_only: bool = False) -> list[str]:
|
|
errors = []
|
|
names = [wheel["name"] for wheel in wheels]
|
|
expected = set(PACKAGE_SOURCES)
|
|
if python_only:
|
|
expected -= {"kt-kernel", "sgl-kernel-kt"}
|
|
if len(names) != len(set(names)) or set(names) != expected:
|
|
errors.append(
|
|
f"Expected one raw wheel per project: {sorted(expected)}; got {names}"
|
|
)
|
|
return errors
|
|
by_name = {wheel["name"]: wheel for wheel in wheels}
|
|
versions = {name: wheel["version"] for name, wheel in by_name.items()}
|
|
if python_only:
|
|
# Both KT projects use the same checked-in version.py. Check pins
|
|
# before spending hours on native builds; verify the real native
|
|
# wheel's version again in the complete audit.
|
|
versions["kt-kernel"] = versions["ktransformers"]
|
|
for name, wheel in by_name.items():
|
|
requirements = active_requirements(wheel)
|
|
for req in requirements:
|
|
dep = canonicalize_name(req.name)
|
|
if dep in {"transformers", "accelerate", "sgl-kernel"}:
|
|
errors.append(
|
|
f"{name} requires conflicting upstream distribution {req}"
|
|
)
|
|
if dep in versions and (req.url or versions[dep] not in req.specifier):
|
|
errors.append(
|
|
f"{name} requires {req}, but this source stack selects {dep}=={versions[dep]}"
|
|
)
|
|
for dep in EXACT_DEPENDENCIES.get(name, ()):
|
|
matches = [r for r in requirements if canonicalize_name(r.name) == dep]
|
|
expected_pin = f"=={versions[dep]}"
|
|
if not matches or any(
|
|
r.url or str(r.specifier) != expected_pin for r in matches
|
|
):
|
|
errors.append(
|
|
f"{name} must pin {dep}{expected_pin} in its main source metadata"
|
|
)
|
|
return errors
|
|
|
|
|
|
def audit(
|
|
lock: dict,
|
|
sources: Path,
|
|
wheels_dir: Path,
|
|
output: Path,
|
|
*,
|
|
python_only: bool = False,
|
|
) -> bool:
|
|
evidence = source_evidence(lock, sources)
|
|
wheels = [inspect_wheel(path) for path in sorted(wheels_dir.rglob("*.whl"))]
|
|
for wheel in wheels:
|
|
key = PACKAGE_SOURCES.get(wheel["name"])
|
|
if key:
|
|
wheel["source"] = evidence[key]
|
|
errors = audit_wheels(wheels, python_only=python_only)
|
|
report = {
|
|
"schema_version": 1,
|
|
"stage": "python-metadata-preflight" if python_only else "raw-native-wheels",
|
|
"source_lock": lock,
|
|
"wheels": wheels,
|
|
"errors": errors,
|
|
"metadata_consistent": not errors,
|
|
"publishable": False,
|
|
"pending_gates": [
|
|
"Assemble fresh SGL CUDA payload into version-parameterized carrier wheels",
|
|
"Resolve complete dependencies into an isolated, hashed wheelhouse",
|
|
"Install the final wheels in the isolated qj5090 runner and run the three-model E2E suite",
|
|
"Promote the exact validated wheel hashes and verify installation from PyPI",
|
|
],
|
|
}
|
|
if python_only:
|
|
report["pending_gates"].insert(
|
|
0, "Build and audit fresh KT and SGL native wheels"
|
|
)
|
|
save_json(output, report)
|
|
for error in errors:
|
|
print(error, file=sys.stderr)
|
|
return not errors
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
freeze = commands.add_parser("snapshot")
|
|
freeze.add_argument("--workflow-sha", required=True)
|
|
freeze.add_argument("--output", type=Path, required=True)
|
|
fetch = commands.add_parser("checkout")
|
|
fetch.add_argument("--lock", type=Path, required=True)
|
|
fetch.add_argument("--destination", type=Path, required=True)
|
|
verify = commands.add_parser("audit")
|
|
verify.add_argument("--lock", type=Path, required=True)
|
|
verify.add_argument("--sources", type=Path, required=True)
|
|
verify.add_argument("--wheels", type=Path, required=True)
|
|
verify.add_argument("--output", type=Path, required=True)
|
|
verify.add_argument("--python-only", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.command == "snapshot":
|
|
save_json(args.output, snapshot(args.workflow_sha))
|
|
elif args.command == "checkout":
|
|
checkout(read_lock(args.lock), args.destination)
|
|
else:
|
|
if not audit(
|
|
read_lock(args.lock),
|
|
args.sources,
|
|
args.wheels,
|
|
args.output,
|
|
python_only=args.python_only,
|
|
):
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|