Download inference_code/inference/pruning/unstructured.py from Agnes-AI/Agnes-Video-2.0-MoE: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/Agnes-AI/Agnes-Video-2.0-MoE/resolve/main/inference_code/inference/pruning/unstructured.py
- Command line
-
hf download hf://Agnes-AI/Agnes-Video-2.0-MoE/inference_code/inference/pruning/unstructured.py
-
curl -L -o unstructured.py https://huggingface.co/Agnes-AI/Agnes-Video-2.0-MoE/resolve/main/inference_code/inference/pruning/unstructured.py
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 | |
| 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 | |