qwen3.5-122B-A10B on DGX Spark: vLLM + DFlash + dense-bandwidth stack, one-shot installer

This commit is contained in:
ent
2026-06-24 13:02:35 +10:00
commit 60bf1b7b02
21 changed files with 2353 additions and 0 deletions
+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""
Build a hybrid GPTQ-INT4 + FP8 checkpoint for Qwen3.5-122B-A10B.
Takes MoE expert weights from the GPTQ-INT4 checkpoint (0.5 bytes/param),
and dense layers (attention, shared experts, embeddings) from the official
FP8 checkpoint (1 byte/param + calibrated block scales).
Result: a checkpoint that is ~9 GB smaller than the GPTQ-INT4 original
while using properly calibrated FP8 scales (not naive cast), yielding
better decode throughput on bandwidth-limited hardware.
NOTE: Requires the hybrid FP8 dispatch patch from https://github.com/rmstxrx/vllm/tree/v0.17.1-hybrid-fp8
Usage:
python build-hybrid-checkpoint.py \
--gptq-dir ~/inference/models/hf/qwen3.5-122b-a10b-gptq-int4 \
--fp8-repo Qwen/Qwen3.5-122B-A10B-FP8 \
--output ~/inference/models/hf/qwen3.5-122b-a10b-fp8hybrid
"""
import argparse
import json
import logging
import shutil
from pathlib import Path
import torch
from huggingface_hub import hf_hub_download
from safetensors import safe_open
from safetensors.torch import load_file, save_file
logger = logging.getLogger(__name__)
def get_fp8_non_expert_manifest(fp8_repo: str) -> dict[str, str]:
"""Get the weight map for non-expert tensors from the FP8 checkpoint.
Args:
fp8_repo: Hugging Face repo ID for the FP8 checkpoint.
Returns:
Mapping of tensor names to shard filenames for non-expert tensors.
"""
idx_path = hf_hub_download(fp8_repo, "model.safetensors.index.json")
with open(idx_path, encoding="utf-8") as f:
idx = json.load(f)
wm = idx["weight_map"]
return {k: v for k, v in wm.items() if ".experts." not in k}
def download_fp8_shards(fp8_repo: str, shards: set[str], cache_dir: Path) -> dict[str, Path]:
"""Download only the needed FP8 shards.
Args:
fp8_repo: Hugging Face repo ID for the FP8 checkpoint.
shards: Shard filenames that contain non-expert tensors.
cache_dir: Local cache directory for downloaded shards.
Returns:
Mapping of shard filename to downloaded local path.
"""
shard_paths: dict[str, Path] = {}
for shard in sorted(shards):
logger.info(" Downloading %s...", shard)
path = hf_hub_download(fp8_repo, shard, local_dir=str(cache_dir))
shard_paths[shard] = Path(path)
logger.info(" -> %s", path)
return shard_paths
def extract_fp8_tensors(shard_paths: dict[str, Path], wanted: dict[str, str]) -> dict[str, torch.Tensor]:
"""Extract the requested FP8 tensors from downloaded shards.
Args:
shard_paths: Mapping of shard filename to local path.
wanted: Mapping of tensor name to shard filename.
Returns:
Mapping of tensor name to loaded FP8 tensor.
"""
tensors: dict[str, torch.Tensor] = {}
for shard_name, shard_path in shard_paths.items():
keys_in_shard = [k for k, v in wanted.items() if v == shard_name]
if not keys_in_shard:
continue
logger.info(" Extracting %d tensors from %s...", len(keys_in_shard), shard_name)
with safe_open(str(shard_path), framework="pt") as f:
for k in keys_in_shard:
tensors[k] = f.get_tensor(k)
return tensors
def find_model_safetensors_files(directory: Path) -> list[Path]:
"""Find model safetensors files in a GPTQ checkpoint directory.
Args:
directory: Directory containing GPTQ checkpoint files.
Returns:
Sorted list of shard paths, or a single-element list for single-file checkpoints.
Raises:
FileNotFoundError: If no supported model safetensors files are found.
"""
gptq_shards = sorted(directory.glob("model.safetensors-*"))
if gptq_shards:
return gptq_shards
# AutoRound / HF naming: model-NNNNN-of-NNNNN.safetensors
gptq_shards = sorted(directory.glob("model-*-of-*.safetensors"))
if gptq_shards:
return gptq_shards
single_file = directory / "model.safetensors"
if single_file.is_file():
return [single_file]
raise FileNotFoundError(
f"No model.safetensors files found in {directory}. Expected a sharded "
"checkpoint (model.safetensors-NNNNN-of-NNNNN)."
)
def validate_gptq_input(gptq_dir: Path) -> None:
"""Validate the GPTQ checkpoint directory before any downloads.
Args:
gptq_dir: Path to the local GPTQ checkpoint directory.
Raises:
FileNotFoundError: If the directory or required files do not exist.
NotADirectoryError: If the path exists but is not a directory.
"""
if not gptq_dir.exists():
raise FileNotFoundError(f"GPTQ directory does not exist: {gptq_dir}")
if not gptq_dir.is_dir():
raise NotADirectoryError(f"GPTQ path is not a directory: {gptq_dir}")
if not any(path.is_file() for path in gptq_dir.glob("*.safetensors*")):
raise FileNotFoundError(f"No .safetensors files found in {gptq_dir}")
if not (gptq_dir / "config.json").is_file():
raise FileNotFoundError(f"Missing config.json in {gptq_dir}")
def validate_output_dir(output_dir: Path, force: bool) -> None:
"""Validate the output directory before building.
Args:
output_dir: Path where the hybrid checkpoint will be written.
force: Whether destructive cleanup is allowed.
Raises:
FileExistsError: If the output directory is non-empty without `force`.
NotADirectoryError: If the output path exists but is not a directory.
"""
if output_dir.exists() and not output_dir.is_dir():
raise NotADirectoryError(f"Output path is not a directory: {output_dir}")
if output_dir.exists() and any(output_dir.iterdir()) and not force:
raise FileExistsError(
f"Output directory {output_dir} exists and is not empty. Use --force "
"to remove existing model.safetensors* and config.json files before building."
)
def prepare_output_dir(output_dir: Path, force: bool) -> None:
"""Create or clean the output directory before writing files.
Args:
output_dir: Path where the hybrid checkpoint will be written.
force: Whether destructive cleanup is allowed.
"""
if not output_dir.exists():
output_dir.mkdir(parents=True, exist_ok=True)
return
if not force:
return
delete_targets = sorted(
{
path
for pattern in ("model.safetensors*", "config.json")
for path in output_dir.glob(pattern)
if path.is_file()
}
)
for path in delete_targets:
path.unlink()
logger.info(" Deleted %s", path)
def build_hybrid_checkpoint(
gptq_dir: Path,
fp8_tensors: dict[str, torch.Tensor],
output_dir: Path,
force: bool,
) -> tuple[int, int, int]:
"""Build the hybrid checkpoint from GPTQ and FP8 tensors.
Args:
gptq_dir: Path to the local GPTQ checkpoint directory.
fp8_tensors: FP8 tensors keyed by tensor name.
output_dir: Output directory for rewritten checkpoint shards.
force: Whether to continue if many unexpected FP8 tensors are unmatched.
Returns:
Tuple of replaced tensor count, added scale tensor count, and bytes saved.
Raises:
FileNotFoundError: If no supported GPTQ model files are found.
RuntimeError: If too many FP8 tensors cannot be matched and `force` is not set.
ValueError: If a matched FP8 tensor has an incompatible shape.
"""
# 1. Copy non-safetensors files
for f in gptq_dir.iterdir():
if f.suffix != ".safetensors" and not f.name.startswith("."):
if f.is_file():
shutil.copy2(f, output_dir / f.name)
# 2. Process each GPTQ shard
gptq_shards = find_model_safetensors_files(gptq_dir)
total_replaced = 0
total_added = 0
total_saved_bytes = 0
# Track which FP8 tensors have been placed
placed_fp8: set[str] = set()
for i, shard_path in enumerate(gptq_shards):
shard_name = shard_path.name
logger.info(" [%d/%d] %s...", i + 1, len(gptq_shards), shard_name)
gptq_tensors = load_file(str(shard_path))
output_tensors: dict[str, torch.Tensor] = {}
replaced = 0
for name, tensor in gptq_tensors.items():
if name in fp8_tensors:
# Replace BF16 tensor with FP8 version
fp8_tensor = fp8_tensors[name]
if tensor.shape != fp8_tensor.shape:
raise ValueError(
f"Shape mismatch for {name}: GPTQ={tensor.shape}, FP8={fp8_tensor.shape}"
)
old_bytes = tensor.numel() * tensor.element_size()
new_bytes = fp8_tensor.numel() * fp8_tensor.element_size()
total_saved_bytes += old_bytes - new_bytes
output_tensors[name] = fp8_tensor
placed_fp8.add(name)
replaced += 1
# Also add the scale tensor if it exists
scale_name = name.replace(".weight", ".weight_scale_inv")
if scale_name in fp8_tensors and scale_name != name:
output_tensors[scale_name] = fp8_tensors[scale_name]
placed_fp8.add(scale_name)
total_added += 1
else:
output_tensors[name] = tensor
total_replaced += replaced
save_file(output_tensors, str(output_dir / shard_name))
logger.info(" replaced=%d, tensors=%d", replaced, len(output_tensors))
# 3. Check for any FP8 tensors not yet placed (e.g. scale tensors
# for weights that exist in shards we already processed)
unplaced = set(fp8_tensors.keys()) - placed_fp8
if unplaced:
expected_unplaced = {
name
for name in unplaced
if name.endswith(".weight_scale_inv")
and f"{name.removesuffix('.weight_scale_inv')}.weight" in placed_fp8
}
unexpected_unplaced = sorted(unplaced - expected_unplaced)
for name in unexpected_unplaced:
logger.warning("WARNING: unexpected unmatched FP8 tensor %s", name)
if len(unexpected_unplaced) > 5 and not force:
message = (
f"{len(unexpected_unplaced)} FP8 tensors could not be matched to GPTQ tensors. "
"This likely indicates a naming mismatch between the GPTQ and FP8 checkpoints. "
"Use --force to proceed anyway."
)
logger.error("ERROR: %s", message)
raise RuntimeError(message)
if len(unexpected_unplaced) > 5 and force:
logger.warning(
"WARNING: proceeding despite %d unexpected unmatched FP8 tensors because --force was provided",
len(unexpected_unplaced),
)
if expected_unplaced:
logger.info(" Adding %d expected unplaced FP8 scale tensors to final shard...", len(expected_unplaced))
if expected_unplaced:
# Load last shard, add expected scale tensors, re-save
last_shard = output_dir / gptq_shards[-1].name
existing = load_file(str(last_shard))
for name in sorted(expected_unplaced):
existing[name] = fp8_tensors[name]
total_added += 1
save_file(existing, str(last_shard))
return total_replaced, total_added, total_saved_bytes
def update_safetensors_index(output_dir: Path) -> None:
"""Rebuild `model.safetensors.index.json` from actual shard contents.
Args:
output_dir: Directory containing rewritten model shards.
"""
weight_map: dict[str, str] = {}
total_size = 0
for shard_path in find_model_safetensors_files(output_dir):
with safe_open(str(shard_path), framework="pt") as f:
for key in f.keys():
weight_map[key] = shard_path.name
tensor = f.get_tensor(key)
total_size += tensor.numel() * tensor.element_size()
index = {
"metadata": {"total_size": total_size},
"weight_map": weight_map
}
with open(output_dir / "model.safetensors.index.json", "w", encoding="utf-8") as f:
json.dump(index, f, indent=2, sort_keys=True)
logger.info(" Index rebuilt: %d tensors, %.2f GB", len(weight_map), total_size / 1e9)
def update_config(output_dir: Path) -> None:
"""Update `config.json` with hybrid quantization metadata.
Args:
output_dir: Directory containing the hybrid checkpoint.
"""
config_path = output_dir / "config.json"
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
config["_hybrid_quant_info"] = {
"description": "Hybrid GPTQ-INT4 + FP8 checkpoint for single-GPU deployment",
"moe_experts": "GPTQ INT4 (group_size=128, sym=True, Marlin kernels)",
"dense_layers": "FP8 E4M3 block-128 (from official Qwen/Qwen3.5-122B-A10B-FP8, calibrated scales)",
"norms_gates_embeddings": "Preserved at source dtype (BF16 for norms/gates, FP8 for others)",
"source_gptq": "Qwen/Qwen3.5-122B-A10B-GPTQ-Int4",
"source_fp8": "Qwen/Qwen3.5-122B-A10B-FP8",
"vllm_patch": "https://github.com/rmstxrx/vllm/tree/v0.17.1-hybrid-fp8",
"target_hardware": "NVIDIA DGX Spark (GB10, 128GB unified, 273 GB/s)",
"converter": "build-hybrid-checkpoint.py"
}
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
def main() -> int:
"""Build a hybrid GPTQ-INT4 + FP8 checkpoint.
Returns:
Process exit code.
"""
logging.basicConfig(level=logging.INFO, format="%(message)s")
parser = argparse.ArgumentParser(description="Build hybrid GPTQ-INT4 + FP8 checkpoint")
parser.add_argument("--gptq-dir", required=True, help="Path to GPTQ-INT4 model")
parser.add_argument("--fp8-repo", default="Qwen/Qwen3.5-122B-A10B-FP8", help="HF repo for FP8 model")
parser.add_argument("--output", required=True, help="Output directory")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--force",
action="store_true",
help="Allow output cleanup and continue despite many unmatched FP8 tensors",
)
args = parser.parse_args()
gptq_dir = Path(args.gptq_dir)
output_dir = Path(args.output)
validate_gptq_input(gptq_dir)
find_model_safetensors_files(gptq_dir)
validate_output_dir(output_dir, args.force)
logger.info("=== Hybrid GPTQ-INT4 + FP8 Checkpoint Builder ===")
logger.info(" GPTQ source: %s", gptq_dir)
logger.info(" FP8 source: %s", args.fp8_repo)
logger.info(" Output: %s", output_dir)
logger.info("")
# Step 1: Get manifest of non-expert tensors from FP8 checkpoint
logger.info("[1/5] Fetching FP8 tensor manifest...")
fp8_manifest = get_fp8_non_expert_manifest(args.fp8_repo)
shards_needed = set(fp8_manifest.values())
logger.info(" Non-expert tensors: %d", len(fp8_manifest))
logger.info(" Shards to download: %s", sorted(shards_needed))
if args.dry_run:
logger.info("")
logger.info("[DRY RUN] Would download shards and build hybrid. Exiting.")
return 0
prepare_output_dir(output_dir, args.force)
cache_dir = output_dir / ".fp8_cache"
cache_dir.mkdir(exist_ok=True)
# Step 2: Download needed FP8 shards
logger.info("")
logger.info("[2/5] Downloading %d FP8 shards...", len(shards_needed))
shard_paths = download_fp8_shards(args.fp8_repo, shards_needed, cache_dir)
# Step 3: Extract non-expert FP8 tensors
logger.info("")
logger.info("[3/5] Extracting FP8 tensors...")
fp8_tensors = extract_fp8_tensors(shard_paths, fp8_manifest)
logger.info(" Extracted: %d tensors", len(fp8_tensors))
# Show dtype breakdown
dtypes: dict[str, int] = {}
for name, t in fp8_tensors.items():
d = str(t.dtype)
dtypes[d] = dtypes.get(d, 0) + 1
logger.info(" Dtypes: %s", dtypes)
# Step 4: Build hybrid checkpoint
logger.info("")
logger.info("[4/5] Building hybrid checkpoint...")
replaced, added, saved = build_hybrid_checkpoint(
gptq_dir,
fp8_tensors,
output_dir,
args.force,
)
# Step 5: Update index and config
logger.info("")
logger.info("[5/5] Updating index and config...")
update_safetensors_index(output_dir)
update_config(output_dir)
# Cleanup downloaded FP8 shards
shutil.rmtree(cache_dir, ignore_errors=True)
logger.info("")
logger.info("=== Complete ===")
logger.info(" Tensors replaced (BF16→FP8): %d", replaced)
logger.info(" Scale tensors added: %d", added)
logger.info(" Bytes saved: %.2f GB", saved / 1e9)
logger.info(" Output: %s", output_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Inspect the Intel INT4 AutoRound checkpoint to decide if albond's hybrid
INT4+FP8 build will work: it swaps BF16 *dense* (non-expert) tensors for FP8 by
name. If AutoRound quantized the dense linears to INT4 (.qweight), the swap is a
near no-op. We need attention/shared_expert dense weights stored as BF16 (.weight).
Also pulls the FP8 repo's index to confirm it exists and that names line up.
"""
import json
import sys
from collections import Counter
from pathlib import Path
INT4_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(
"/home/ent/.cache/huggingface/hub/models--Intel--Qwen3.5-122B-A10B-int4-AutoRound/"
"snapshots/3045d02bb737effc4581da91bddbad3be02934e4")
FP8_REPO = sys.argv[2] if len(sys.argv) > 2 else "Qwen/Qwen3.5-122B-A10B-FP8"
idx = json.loads((INT4_DIR / "model.safetensors.index.json").read_text())
wm = idx["weight_map"]
names = list(wm.keys())
# Non-expert = no ".experts." (routed experts stay INT4 in the hybrid)
nonexp = [n for n in names if ".experts." not in n]
def bucket(n):
if ".experts." in n:
return "ROUTED-EXPERT"
if "shared_expert" in n:
return "shared_expert"
if "self_attn" in n or "linear_attn" in n or ".attn" in n:
return "attention"
if "embed_tokens" in n or "lm_head" in n:
return "embed/head"
if "mtp" in n:
return "mtp"
return "other"
print(f"=== Intel INT4 checkpoint: {INT4_DIR.name} ===")
print(f"total tensors: {len(names)} non-expert: {len(nonexp)}")
# Suffix histogram tells us quant scheme: .qweight/.scales/.qzeros => INT4; .weight => dense
suffix = Counter(n.rsplit(".", 1)[-1] for n in nonexp)
print("\nnon-expert tensor SUFFIX histogram (qweight/scales/qzeros = INT4-packed; weight = dense):")
for s, c in suffix.most_common():
print(f" .{s:20s} {c}")
# For each functional group, does it have .weight (BF16 dense) or .qweight (INT4)?
print("\nper-group quant scheme (sample names):")
groups = {}
for n in nonexp:
g = bucket(n)
groups.setdefault(g, {"weight": 0, "qweight": 0, "scale": 0, "other": 0, "ex": None})
suf = n.rsplit(".", 1)[-1]
if suf == "weight":
groups[g]["weight"] += 1
elif suf == "qweight":
groups[g]["qweight"] += 1
elif "scale" in suf or suf in ("qzeros",):
groups[g]["scale"] += 1
else:
groups[g]["other"] += 1
if groups[g]["ex"] is None and suf in ("weight", "qweight"):
groups[g]["ex"] = n
for g, d in sorted(groups.items()):
scheme = "INT4(.qweight)" if d["qweight"] else ("DENSE(.weight)" if d["weight"] else "?")
print(f" {g:16s} weight={d['weight']:4d} qweight={d['qweight']:4d} scale={d['scale']:4d} -> {scheme}")
print(f" e.g. {d['ex']}")
# Dtypes of a few non-expert .weight tensors (open the shard header only).
print("\ndtypes of sample non-expert '.weight' tensors (FP8 swap needs BF16 here):")
from safetensors import safe_open # noqa: E402
sample = [n for n in nonexp if n.endswith(".weight")
and ("self_attn" in n or "shared_expert" in n or "embed" in n or "lm_head" in n)]
seen_shards = {}
shown = 0
for n in sample:
shard = wm[n]
f = seen_shards.get(shard)
if f is None:
f = safe_open(str(INT4_DIR / shard), framework="pt")
seen_shards[shard] = f
try:
t = f.get_slice(n)
print(f" {n:60s} {t.get_dtype()} {tuple(t.get_shape())}")
except Exception as e:
print(f" {n:60s} <err {e}>")
shown += 1
if shown >= 12:
break
# FP8 repo: confirm exists + list its non-expert names/dtypes for name-match sanity.
print(f"\n=== FP8 repo manifest: {FP8_REPO} ===")
try:
from huggingface_hub import hf_hub_download
p = hf_hub_download(FP8_REPO, "model.safetensors.index.json")
fidx = json.loads(Path(p).read_text())
fwm = fidx["weight_map"]
fnon = [n for n in fwm if ".experts." not in n]
print(f"FP8 total tensors: {len(fwm)} non-expert: {len(fnon)}")
fsuf = Counter(n.rsplit('.', 1)[-1] for n in fnon)
print("FP8 non-expert suffix histogram:")
for s, c in fsuf.most_common(10):
print(f" .{s:20s} {c}")
# how many FP8 non-expert .weight names also exist in INT4 as .weight?
int4_weight = {n for n in nonexp if n.endswith('.weight')}
fp8_weight = {n for n in fnon if n.endswith('.weight')}
match = int4_weight & fp8_weight
print(f"\nname overlap (FP8 '.weight' that also exist as '.weight' in INT4): {len(match)} / {len(fp8_weight)} FP8 weights")
only_fp8 = sorted(fp8_weight - int4_weight)[:8]
print(f"FP8 '.weight' NOT present as '.weight' in INT4 (would not swap): {len(fp8_weight - int4_weight)}")
for n in only_fp8:
print(f" {n} (INT4 has: {'qweight' if n[:-7]+'.qweight' in set(names) else 'MISSING'})")
except Exception as e:
print(f"<FP8 repo fetch failed: {e}>")
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""v2 harness: proper bf16 GEMV baseline + a single BATCHED int8 w8a16 GEMM kernel
(one launch for any B, dot-based, pads B to >=16) vs albond's per-row loop, at the
real lm-head shape and B in {1,5,13} (base + DFlash verify-batch sizes).
"""
import time
import torch
import triton
import triton.language as tl
V, H = 248320, 3072
DEV = "cuda"
torch.manual_seed(0)
W_bf16 = (torch.randn(V, H, device=DEV, dtype=torch.float32) * 0.02).to(torch.bfloat16)
scales = (W_bf16.float().abs().amax(dim=1) / 127.0).clamp(min=1e-12)
W_int8 = (W_bf16.float() / scales.unsqueeze(1)).round().clamp(-127, 127).to(torch.int8)
scales_f16 = scales.to(torch.float16)
INT8_BYTES = V * H
BF16_BYTES = V * H * 2
def bench(fn, iters=30, warmup=5):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(iters):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - t0) / iters
# ---- batched int8 w8a16 GEMM: out[B,N] = (x[B,K] @ (W_int8[N,K]*s[N]).T) ----
@triton.jit
def _k_batched(x_ptr, w_ptr, s_ptr, o_ptr, B, N, K,
sxb, sxk, swn, swk, sob, son,
BLOCK_B: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr):
pid_n = tl.program_id(0)
offs_b = tl.arange(0, BLOCK_B)
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
offs_k = tl.arange(0, BLOCK_K)
x_ptrs = x_ptr + offs_b[:, None] * sxb + offs_k[None, :] * sxk
w_ptrs = w_ptr + offs_n[:, None] * swn + offs_k[None, :] * swk
acc = tl.zeros((BLOCK_B, BLOCK_N), dtype=tl.float32)
for k in range(0, K, BLOCK_K):
km = (offs_k[None, :] + k) < K
x = tl.load(x_ptrs, mask=(offs_b[:, None] < B) & km, other=0.0).to(tl.float16)
w = tl.load(w_ptrs, mask=(offs_n[:, None] < N) & km, other=0).to(tl.float16)
acc += tl.dot(x, w.T) # [BB,BK] @ [BK,BN] -> [BB,BN]
x_ptrs += BLOCK_K * sxk
w_ptrs += BLOCK_K * swk
s = tl.load(s_ptr + offs_n, mask=offs_n < N, other=0.0).to(tl.float32)
acc = acc * s[None, :]
o_ptrs = o_ptr + offs_b[:, None] * sob + offs_n[None, :] * son
tl.store(o_ptrs, acc.to(tl.float16), mask=(offs_b[:, None] < B) & (offs_n[None, :] < N))
def run_batched(x, BLOCK_N=128, BLOCK_K=64, num_warps=4, num_stages=3):
B = x.shape[0]
BLOCK_B = max(16, triton.next_power_of_2(B))
out = torch.empty(B, V, dtype=torch.float16, device=DEV)
xf = x.to(torch.float16)
grid = ((V + BLOCK_N - 1) // BLOCK_N,)
_k_batched[grid](xf, W_int8, scales_f16, out, B, V, H,
xf.stride(0), xf.stride(1), W_int8.stride(0), W_int8.stride(1),
out.stride(0), out.stride(1),
BLOCK_B=BLOCK_B, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K,
num_warps=num_warps, num_stages=num_stages)
return out
for B in (1, 5, 13):
print(f"\n=== B={B} ===")
x = torch.randn(B, H, device=DEV, dtype=torch.bfloat16) * 0.1
ref_bf16 = (x.float() @ W_bf16.float().T)
ref_deq = (x.float() @ (W_int8.float() * scales.unsqueeze(1)).T)
am_floor = (ref_bf16.argmax(-1) == ref_deq.argmax(-1)).float().mean().item()
print(f" quant floor: argmax_match={am_floor*100:.1f}% maxerr={(ref_deq-ref_bf16).abs().max():.4f}")
# real bf16 GEMV baseline (stays bf16, reads 1.5GB)
dt = bench(lambda: torch.matmul(x, W_bf16.t()))
print(f" {'bf16 GEMV (real baseline)':30s} {'':33s}{dt*1e3:7.3f} ms {BF16_BYTES/dt/1e9:6.1f} GB/s")
# batched int8 kernel, a few configs
for (bn, bk, nw, ns) in [(128, 64, 4, 3), (256, 64, 8, 3), (128, 128, 4, 3), (64, 128, 4, 3)]:
try:
out = run_batched(x, bn, bk, nw, ns)
err = (out.float() - ref_deq.float()).abs().max().item()
am = (out.float().argmax(-1) == ref_bf16.argmax(-1)).float().mean().item()
dt = bench(lambda: run_batched(x, bn, bk, nw, ns))
print(f" batched N{bn}/K{bk}/w{nw}/s{ns:<2d} maxerr={err:8.4f} argmax={am*100:5.1f}% "
f"{dt*1e3:7.3f} ms {INT8_BYTES/dt/1e9:6.1f} GB/s")
except Exception as e:
print(f" batched N{bn}/K{bk}/w{nw}/s{ns}: ERR {str(e)[:70]}")
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Validate patch_inc_hybrid.py landed on AEON 0.23's inc.py. Run AFTER the patch
in the same container: python3 /host/patch_inc_hybrid.py && python3 /host/validate_inc_patch.py
"""
import inspect
import vllm.model_executor.layers.quantization.inc as m
C = m.INCConfig
src = inspect.getsource(m)
print("import OK")
print("sentinel in source :", "spark-dflash-hybrid-fp8" in src)
print("maybe_update_config OWN :", "maybe_update_config" in C.__dict__)
print("_is_layer_fp8 OWN :", "_is_layer_fp8" in C.__dict__)
print("maybe_update_config hasattr:", hasattr(C, "maybe_update_config"))
print("_is_layer_fp8 hasattr :", hasattr(C, "_is_layer_fp8"))
# signature must accept hf_config kw (config/vllm.py calls it that way)
try:
sig = inspect.signature(C.maybe_update_config)
print("maybe_update_config sig :", str(sig))
print("accepts hf_config kw :", "hf_config" in sig.parameters)
except Exception as e:
print("sig err:", e)
# count FP8 dispatch sites
print("Fp8LinearMethod dispatch ct:", src.count("return Fp8LinearMethod(self.fp8_config)"))
# byte-compile sanity already implied by import; show line count
print("inc.py lines :", len(src.splitlines()))
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Validate patch_int8_lmhead_v3 landed + the helper runs on GPU. Run AFTER the
patch in the same container."""
import torch
import vllm.model_executor.layers.logits_processor as lp
print("import OK | helpers:",
hasattr(lp, "_spark_int8_gemm"),
hasattr(lp, "_spark_int8_lmhead_apply"),
hasattr(lp, "_spark_k_int8"))
src = open(lp.__file__).read()
print("sentinel in _get_logits:", "DGX_SPARK_INT8_LMHEAD_V3: int8 w8a16" in src)
V, H = 4096, 512
torch.manual_seed(0)
W = torch.randn(V, H, device="cuda") * 0.02
s = (W.abs().amax(1) / 127).clamp(min=1e-12)
wi = (W / s.unsqueeze(1)).round().clamp(-127, 127).to(torch.int8).contiguous()
sf = s.to(torch.float16)
for B in (1, 5, 13):
x = torch.randn(B, H, device="cuda", dtype=torch.bfloat16) * 0.1
out = lp._spark_int8_gemm(x, wi, sf)
ref = x.float() @ (wi.float() * s.unsqueeze(1)).T
am = (out.argmax(-1) == ref.argmax(-1)).float().mean().item()
print(f" B={B}: out{tuple(out.shape)} {out.dtype} argmax={am*100:.0f}% "
f"maxerr={(out - ref).abs().max().item():.4f}")
# Exercise the FULL apply path (print + quantize-once + gemm) with a mock lm_head
# (vocab > 100k to trigger the int8 path). self is unused -> None.
print("--- full _spark_int8_lmhead_apply path (mock lm_head, V=131072) ---")
Vbig = 131072
class _MockLMHead:
pass
mh = _MockLMHead()
mh.weight = (torch.randn(Vbig, H, device="cuda", dtype=torch.float32) * 0.02).to(torch.bfloat16)
hs = torch.randn(3, H, device="cuda", dtype=torch.bfloat16) * 0.1
o1 = lp._spark_int8_lmhead_apply(None, mh, hs, None) # first call: quantizes + prints
o2 = lp._spark_int8_lmhead_apply(None, mh, hs, None) # second call: reuses int8
refb = hs.float() @ mh.weight.float().T
am = (o2.argmax(-1) == refb.argmax(-1)).float().mean().item()
print(f" apply: out{tuple(o2.shape)} {o2.dtype} argmax_vs_bf16={am*100:.0f}% "
f"int8_ready={getattr(mh, '_spark_int8_ready', None)} weight_kept={mh.weight.numel() > 0}")