Akahsizrr's picture
Publish compatibility source compatibility/fuse2_quantize.py
194afb6 verified
Raw
History Blame Contribute Delete
9.07 kB
from __future__ import annotations
import json
import os
import shutil
import time
from pathlib import Path
import modal
REPO = "Akahsizrr/Mini-Whale-Flash"
SUBDIR = "merged-v2-full"
VOL_NAME = "fuse2-model-store"
VOL_MOUNT = "/data"
MODEL_CACHE = f"{VOL_MOUNT}/hf-models/mini-whale-flash"
OUTPUT_ROOT = f"{VOL_MOUNT}/quantized"
vol = modal.Volume.from_name(VOL_NAME, create_if_missing=True)
image = (
modal.Image.debian_slim(python_version="3.11")
.pip_install("torch==2.7.0", index_url="https://download.pytorch.org/whl/cu126")
.pip_install(
"transformers==5.14.1",
"accelerate==1.2.1",
"bitsandbytes==0.49.1",
"safetensors==0.8.0",
"huggingface_hub",
)
.add_local_file(str(Path(__file__).resolve().parent.parent / "microscope" / "fuse2_model.py"), "/root/fuse2_model.py")
)
app = modal.App("fuse2-quantize")
def _quantization_config(bits: int, quant_type: str = "nf4"):
from transformers import BitsAndBytesConfig
if bits == 8:
return BitsAndBytesConfig(load_in_8bit=True)
if bits == 4 and quant_type in {"nf4", "fp4"}:
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type=quant_type,
bnb_4bit_compute_dtype=__import__("torch").bfloat16,
bnb_4bit_use_double_quant=True,
)
raise ValueError("supported formats are 4-bit nf4, 4-bit fp4, and 8-bit int8")
def _apply_runtime_fixes(model):
import torch
import torch.nn.functional as F
scaled_count = 0
for layer in model.model.layers:
experts = getattr(layer, "experts", None)
if experts is None:
continue
for expert in experts:
gate_proj = getattr(expert, "gate_proj", None)
if gate_proj is None:
continue
weight = gate_proj.weight
if hasattr(weight, "dequantize"):
weight = weight.dequantize()
std_val = weight.float().std().item()
scale = 0.025 / std_val if std_val > 1.0 else 1.0
if scale != 1.0:
scaled_count += 1
expert._fuse2_scale = scale
patched_count = 0
for layer in model.model.layers:
experts = getattr(layer, "experts", None)
if experts is None:
continue
for expert in experts:
gate_proj = expert.gate_proj
up_proj = expert.up_proj
down_proj = expert.down_proj
scale = getattr(expert, "_fuse2_scale", 1.0)
def clamped_forward(x, gp=gate_proj, up=up_proj, dp=down_proj, s=scale):
value = torch.clamp(F.silu(gp(x) * s) * (up(x) * s), -10.0, 10.0)
return dp(value) * s
expert.forward = clamped_forward
patched_count += 1
if not patched_count:
raise RuntimeError("Fuse-2 experts were not found")
return scaled_count, patched_count
@app.function(
image=image,
gpu="A100",
cpu=8,
memory=65536,
timeout=7200,
volumes={VOL_MOUNT: vol},
)
def quantize(bits: int = 4, quant_type: str = "nf4", force: bool = False):
import torch
from huggingface_hub import snapshot_download
from transformers import AutoModelForCausalLM, AutoTokenizer
if bits not in (4, 8):
raise ValueError("bits must be 4 or 8")
if bits == 8:
quant_type = "int8"
output_name = "fuse2-8bit-bnb"
elif quant_type == "nf4":
output_name = "fuse2-4bit-bnb"
else:
output_name = f"fuse2-4bit-{quant_type}-bnb"
output_dir = Path(OUTPUT_ROOT) / output_name
marker = output_dir / "quantization_metadata.json"
if marker.exists() and not force:
return {"status": "exists", "path": str(output_dir), "bits": bits}
model_path = Path(
snapshot_download(
REPO,
allow_patterns=[f"{SUBDIR}/*"],
local_dir=MODEL_CACHE,
)
) / SUBDIR
shutil.copy2("/root/fuse2_model.py", model_path / "fuse2_model.py")
output_dir.mkdir(parents=True, exist_ok=True)
started = time.time()
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_path,
trust_remote_code=True,
quantization_config=_quantization_config(bits, quant_type),
device_map="cuda:0",
torch_dtype=torch.bfloat16,
)
model.eval()
scaled_experts, patched_experts = _apply_runtime_fixes(model)
text = tokenizer.apply_chat_template(
[{"role": "user", "content": "Say hello in one sentence."}],
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(text, return_tensors="pt").to("cuda:0")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=16,
do_sample=False,
use_cache=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
sample = tokenizer.decode(
outputs[0, inputs["input_ids"].shape[1] :], skip_special_tokens=True
)
if not sample and outputs.shape[-1] <= inputs["input_ids"].shape[-1]:
raise RuntimeError("quantized model generated no tokens")
model.save_pretrained(
output_dir,
safe_serialization=True,
max_shard_size="4GB",
)
tokenizer.save_pretrained(output_dir)
source_code = model_path / "fuse2_model.py"
if source_code.exists():
shutil.copy2(source_code, output_dir / "fuse2_model.py")
metadata = {
"base_model": REPO,
"base_subdir": SUBDIR,
"quantization": "bitsandbytes",
"bits": bits,
"quant_type": quant_type,
"compute_dtype": "bfloat16",
"gpu_validation": "NVIDIA A100-SXM4-40GB",
"runtime_fixes": {
"scaled_shared_experts": scaled_experts,
"clamped_experts": patched_experts,
},
"sample_output": sample,
"parameter_count": sum(p.numel() for p in model.parameters()),
"elapsed_seconds": round(time.time() - started, 1),
}
(output_dir / "quantization_metadata.json").write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
vol.commit()
return {"status": "created", "path": str(output_dir), **metadata}
@app.function(
image=image,
gpu="A100",
cpu=8,
memory=65536,
timeout=7200,
volumes={VOL_MOUNT: vol},
)
def validate(bits: int = 4, quant_type: str = "nf4"):
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
if bits not in (4, 8):
raise ValueError("bits must be 4 or 8")
if bits == 8:
quant_type = "int8"
output_name = "fuse2-8bit-bnb"
elif quant_type == "nf4":
output_name = "fuse2-4bit-bnb"
else:
output_name = f"fuse2-4bit-{quant_type}-bnb"
output_dir = Path(OUTPUT_ROOT) / output_name
if not (output_dir / "quantization_metadata.json").exists():
raise FileNotFoundError(output_dir)
tokenizer = AutoTokenizer.from_pretrained(output_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
output_dir,
trust_remote_code=True,
quantization_config=_quantization_config(bits, quant_type),
device_map="cuda:0",
torch_dtype=torch.bfloat16,
)
model.eval()
scaled_experts, patched_experts = _apply_runtime_fixes(model)
text = tokenizer.apply_chat_template(
[{"role": "user", "content": "Say hello in one sentence."}],
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(text, return_tensors="pt").to("cuda:0")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=16,
do_sample=False,
use_cache=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
sample = tokenizer.decode(
outputs[0, inputs["input_ids"].shape[1] :], skip_special_tokens=True
)
if not sample:
raise RuntimeError("saved quantized artifact generated no text")
result = {
"status": "validated",
"path": str(output_dir),
"bits": bits,
"sample_output": sample,
"scaled_shared_experts": scaled_experts,
"clamped_experts": patched_experts,
}
(output_dir / "validation.json").write_text(
json.dumps(result, indent=2) + "\n", encoding="utf-8"
)
vol.commit()
return result
@app.local_entrypoint()
def main(
action: str = "quantize",
bits: int = 4,
quant_type: str = "nf4",
force: bool = False,
):
if action == "quantize":
result = quantize.remote(bits=bits, quant_type=quant_type, force=force)
elif action == "validate":
result = validate.remote(bits=bits, quant_type=quant_type)
else:
raise ValueError("action must be quantize or validate")
print(json.dumps(result, indent=2))