Agnes-AI's picture
Add inference code
fc3971e verified
Raw History Blame Contribute Delete
10.5 kB
"""Unstructured Wanda / SparseGPT on SparseMoE expert Linears.
Shapes are unchanged. Only expert `up_gate_proj` and `down_proj` are pruned.
Layer-sequential: prune block i, then re-forward before scoring block i+1.
"""
from __future__ import annotations
import json
import os
import types
from typing import Any
import torch
import torch.nn as nn
from inference.model.dit.dit_module import SparseMoEMLP
from .sparsegpt_full import SparseGPT
from .wanda_unstructured import WrappedGPT, prune_linear_wanda
FORMAT = "davinci_unstructured_v1"
CAPTURE_ENV = "MAGI_UNSTRUCTURED_CAPTURE"
OUT_ROOT_ENV = "MAGI_UNSTRUCTURED_OUT_ROOT"
METHODS_ENV = "MAGI_UNSTRUCTURED_METHODS"
SPARSITIES_ENV = "MAGI_UNSTRUCTURED_SPARSITIES"
STRIDE_ENV = "MAGI_UNSTRUCTURED_STRIDE"
MAX_ENV = "MAGI_UNSTRUCTURED_MAX"
SOURCE_ENV = "MAGI_UNSTRUCTURED_SOURCE_CKPT"
__all__ = [
"FORMAT",
"moe_expert_linears",
"prune_linear_wanda",
"sequential_unstructured",
"global_mlp_sparsity",
"maybe_attach_capture",
"maybe_prune_captured",
]
def moe_expert_linears(mlp: SparseMoEMLP) -> dict[str, nn.Linear]:
out: dict[str, nn.Linear] = {}
for eid, expert in enumerate(mlp.experts):
out[f"expert{eid}.up_gate_proj"] = expert.up_gate_proj
out[f"expert{eid}.down_proj"] = expert.down_proj
return out
def global_mlp_sparsity(model) -> float:
total = 0
zeros = 0
for layer in model.block.layers:
mlp = getattr(layer, "mlp", None)
if not isinstance(mlp, SparseMoEMLP):
continue
for lin in moe_expert_linears(mlp).values():
w = lin.weight.data
total += int(w.numel())
zeros += int((w == 0).sum())
return zeros / max(total, 1)
def _run_layer(layer, sample: dict[str, Any]):
with torch.amp.autocast("cuda", dtype=torch.bfloat16):
out = layer(
sample["h"],
sample["rope"],
sample["permute_mapping"],
sample["inv_permute_mapping"],
sample["varlen_handler"],
sample["local_attn_handler"],
sample["modality_dispatcher"],
sample["cp_split_sizes"],
)
return out.detach() if torch.is_tensor(out) else out
def _clone_samples(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
cloned = []
for sample in samples:
item = dict(sample)
item["h"] = sample["h"].clone()
cloned.append(item)
return cloned
@torch.no_grad()
def sequential_unstructured(
model,
samples: list[dict[str, Any]],
sparsity: float,
method: str,
verbose: bool = True,
) -> dict:
method = method.strip().lower()
if method not in ("wanda", "sparsegpt"):
raise ValueError(f"method must be wanda or sparsegpt, got {method}")
# Grouped GEMM calls F.linear on packed weights and skips nn.Linear.forward,
# so Wanda/SparseGPT hooks would see nsamples=0 and zero the expert.
os.environ["MAGI_MOE_GROUPED"] = "off"
work = _clone_samples(samples)
layer_meta: dict[str, Any] = {}
captured_any = False
for layer_idx, layer in enumerate(model.block.layers):
mlp = getattr(layer, "mlp", None)
is_moe = isinstance(mlp, SparseMoEMLP)
linears = moe_expert_linears(mlp) if is_moe else {}
wrappers: dict[str, Any] = {}
handles = []
if is_moe:
if method == "wanda":
wrappers = {name: WrappedGPT(lin) for name, lin in linears.items()}
else:
wrappers = {name: SparseGPT(lin) for name, lin in linears.items()}
def _make_hook(wrapper):
def _hook(_, inp, out):
wrapper.add_batch(inp[0].data, out.data)
return _hook
for name, lin in linears.items():
handles.append(lin.register_forward_hook(_make_hook(wrappers[name])))
for sample in work:
_run_layer(layer, sample)
for handle in handles:
handle.remove()
names_meta = {}
layer_ns = [int(wrappers[name].nsamples) for name in linears]
if sum(layer_ns) == 0:
raise RuntimeError(
f"unstructured {method} layer {getattr(mlp, 'layer_idx', layer_idx)} "
"captured 0 expert activations; MAGI_MOE_GROUPED must be off"
)
captured_any = True
for name, lin in linears.items():
nsamples = int(wrappers[name].nsamples)
if nsamples == 0:
names_meta[name] = {
"sparsity": 0.0,
"shape": list(lin.weight.shape),
"nsamples": 0,
"skipped": True,
}
if hasattr(wrappers[name], "free"):
wrappers[name].free()
continue
if method == "wanda":
sp = prune_linear_wanda(lin, wrappers[name].scaler_row, sparsity)
else:
sp = wrappers[name].fasterprune(sparsity, verbose=verbose)
wrappers[name].free()
names_meta[name] = {
"sparsity": float(sp),
"shape": list(lin.weight.shape),
"nsamples": nsamples,
}
layer_meta[str(int(getattr(mlp, "layer_idx", layer_idx)))] = names_meta
for sample in work:
sample["h"] = _run_layer(layer, sample)
if verbose and is_moe:
print(
f"unstructured {method} layer {getattr(mlp, 'layer_idx', layer_idx)} "
f"sparsity={sparsity:.3f}",
flush=True,
)
torch.cuda.empty_cache()
if not captured_any:
raise RuntimeError(f"unstructured {method} captured 0 expert activations")
return {
"method": method,
"sparsity": float(sparsity),
"keep_ratio": float(1.0 - sparsity),
"global_mlp_sparsity": global_mlp_sparsity(model),
"n_samples": len(samples),
"layers": layer_meta,
}
def maybe_attach_capture(model) -> dict | None:
flag = os.getenv(CAPTURE_ENV, "").strip()
if flag not in ("1", "true", "True", "yes"):
return None
stride = int(os.getenv(STRIDE_ENV, "8") or "8")
max_samples = int(os.getenv(MAX_ENV, "40") or "40")
block = model.block
orig = block.forward
bag: dict[str, Any] = {"samples": [], "calls": 0, "orig": orig}
def wrapped(
self,
x,
rope,
permute_mapping,
inv_permute_mapping,
varlen_handler,
local_attn_handler,
modality_dispatcher,
cp_split_sizes,
):
bag["calls"] += 1
if bag["calls"] % stride == 0 and len(bag["samples"]) < max_samples:
bag["samples"].append(
{
"h": x.detach().clone(),
"rope": rope,
"permute_mapping": permute_mapping,
"inv_permute_mapping": inv_permute_mapping,
"varlen_handler": varlen_handler,
"local_attn_handler": local_attn_handler,
"modality_dispatcher": modality_dispatcher,
"cp_split_sizes": cp_split_sizes,
}
)
return orig(
x,
rope,
permute_mapping,
inv_permute_mapping,
varlen_handler,
local_attn_handler,
modality_dispatcher,
cp_split_sizes,
)
block.forward = types.MethodType(wrapped, block)
model._unstr_capture = bag
print(
f"unstructured capture stride={stride} max={max_samples}",
flush=True,
)
return bag
def save_unstructured_checkpoint(path: str, model, meta: dict) -> None:
payload = {
"format": FORMAT,
"model": model.state_dict(),
"unstructured_meta": meta,
}
os.makedirs(os.path.dirname(os.path.abspath(path)) or ".", exist_ok=True)
torch.save(payload, path)
def maybe_prune_captured(model) -> list[str]:
out_root = os.getenv(OUT_ROOT_ENV, "").strip()
if not out_root:
return []
bag = getattr(model, "_unstr_capture", None)
if not bag or not bag.get("samples"):
raise RuntimeError(
f"{OUT_ROOT_ENV} is set but no TransformerBlock inputs were captured"
)
block = model.block
orig = bag.get("orig")
if orig is not None:
block.forward = orig
methods = [m.strip() for m in os.getenv(METHODS_ENV, "wanda,sparsegpt").split(",") if m.strip()]
sparsities = [
float(x) for x in os.getenv(SPARSITIES_ENV, "0.25,0.50,0.75").split(",") if x.strip()
]
source = os.getenv(SOURCE_ENV, "").strip()
samples = bag["samples"]
print(
f"unstructured prune captured={len(samples)} calls={bag.get('calls')} "
f"methods={methods} sparsities={sparsities}",
flush=True,
)
orig_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
written: list[str] = []
os.makedirs(out_root, exist_ok=True)
for method in methods:
for sparsity in sparsities:
keep = 1.0 - float(sparsity)
slug = f"{method}_unstr_keep{int(round(keep * 100)):03d}"
out_path = os.path.join(out_root, f"{slug}.pt")
if os.path.isfile(out_path):
print(f"[skip prune] {out_path} exists", flush=True)
written.append(out_path)
continue
print(f"unstructured apply {slug}", flush=True)
model.load_state_dict(orig_state, strict=True)
meta = sequential_unstructured(
model, samples, sparsity=float(sparsity), method=method, verbose=True,
)
meta["source"] = source
meta["captured_calls"] = int(bag.get("calls") or 0)
save_unstructured_checkpoint(out_path, model, meta)
meta_json = os.path.splitext(out_path)[0] + "_meta.json"
with open(meta_json, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
f.write("\n")
print(f"wrote {out_path} global_mlp_sparsity={meta['global_mlp_sparsity']:.4f}", flush=True)
written.append(out_path)
torch.cuda.empty_cache()
return written