WaveCut's picture
Sync the source snapshot with iamwavecut/OrbitQuant@2d586f189ab8
57c2394 verified
Raw
History Blame Contribute Delete
10.8 kB
from __future__ import annotations
import argparse
import json
import os
import platform
import statistics
import time
import torch
from orbitquant_packed_matmul import matmul_packed_weight
def _pack(values: torch.Tensor, bits: int) -> torch.Tensor:
flat = values.detach().to(device="cpu", dtype=torch.uint8).flatten()
packed = torch.zeros((flat.numel() * bits + 7) // 8, dtype=torch.uint8)
for value_index, value in enumerate(flat.tolist()):
bit_start = value_index * bits
byte_index = bit_start // 8
shift = bit_start % 8
packed[byte_index] |= (value << shift) & 0xFF
if shift + bits > 8:
packed[byte_index + 1] |= value >> (8 - shift)
return packed
def _synchronize(device: str) -> None:
if device == "cuda":
torch.cuda.synchronize()
elif device == "mps":
torch.mps.synchronize()
def _time_call(device: str, fn) -> float:
_synchronize(device)
start = time.perf_counter_ns()
fn()
_synchronize(device)
return (time.perf_counter_ns() - start) / 1_000_000_000
def _time_distribution(device: str, iters: int, fn) -> dict[str, float]:
samples = []
for _ in range(iters):
samples.append(_time_call(device, fn))
samples.sort()
return {
"mean": statistics.fmean(samples),
"median": statistics.median(samples),
"p95": samples[min(len(samples) - 1, int(len(samples) * 0.95))],
}
def _parse_rows(raw: str) -> list[int]:
rows_values = []
for chunk in raw.split(","):
chunk = chunk.strip()
if not chunk:
continue
rows = int(chunk)
if rows <= 0:
raise argparse.ArgumentTypeError("--rows values must be positive")
rows_values.append(rows)
if not rows_values:
raise argparse.ArgumentTypeError("--rows must list at least one row count")
return rows_values
_DTYPES = {
"float32": torch.float32,
"float16": torch.float16,
"bfloat16": torch.bfloat16,
}
def _benchmark_rows(args, rows: int, dtype: torch.dtype, weights) -> dict:
packed, row_norms, centroids, indices_device = weights
x = torch.randn(rows, args.in_features, device=args.device, dtype=dtype)
bias = (
torch.randn(args.out_features, device=args.device, dtype=dtype)
if args.with_bias
else None
)
def materialize_reference_weight() -> torch.Tensor:
return (row_norms[:, None] * centroids[indices_device]).to(dtype)
reference_weight = materialize_reference_weight()
packed_weight_indices_bytes = packed.numel() * packed.element_size()
row_norms_bytes = row_norms.numel() * row_norms.element_size()
centroid_bytes = centroids.numel() * centroids.element_size()
packed_weight_path_bytes = packed_weight_indices_bytes + row_norms_bytes + centroid_bytes
materialized_weight_bytes = reference_weight.numel() * reference_weight.element_size()
def packed_call() -> torch.Tensor:
return matmul_packed_weight(
x,
packed,
row_norms,
centroids,
bits=args.bits,
out_features=args.out_features,
in_features=args.in_features,
bias=bias,
)
def predequantized_linear_call() -> torch.Tensor:
return torch.nn.functional.linear(x, reference_weight, bias)
def dequantize_then_linear_call() -> torch.Tensor:
return torch.nn.functional.linear(x, materialize_reference_weight(), bias)
packed_first_call_seconds = _time_call(args.device, packed_call)
predequantized_first_call_seconds = _time_call(args.device, predequantized_linear_call)
dequantize_then_first_call_seconds = _time_call(args.device, dequantize_then_linear_call)
for _ in range(args.warmup):
packed_call()
predequantized_linear_call()
dequantize_then_linear_call()
packed_distribution = _time_distribution(args.device, args.iters, packed_call)
predequantized_distribution = _time_distribution(
args.device,
args.iters,
predequantized_linear_call,
)
dequantize_then_distribution = _time_distribution(
args.device,
args.iters,
dequantize_then_linear_call,
)
# Headline numbers are hot-loop medians; the mean is retained alongside the
# median/p95 so noisy first-iteration outliers cannot skew comparisons.
packed_seconds = packed_distribution["median"]
predequantized_linear_seconds = predequantized_distribution["median"]
dequantize_then_linear_seconds = dequantize_then_distribution["median"]
packed_output = packed_call()
reference_output = predequantized_linear_call()
_synchronize(args.device)
error = packed_output.float() - reference_output.float()
max_abs_error = error.abs().max().item()
rmse = error.square().mean().sqrt().item()
reference_rms = reference_output.float().square().mean().sqrt().item()
relative_rmse = rmse / max(reference_rms, 1e-12)
return {
"device": args.device,
"device_name": (
torch.cuda.get_device_name(0)
if args.device == "cuda"
else "mps"
if args.device == "mps"
else f"{platform.processor() or platform.machine()} "
f"({torch.backends.cpu.get_cpu_capability()})"
),
"dtype": str(dtype).replace("torch.", ""),
"bits": args.bits,
"rows": rows,
"in_features": args.in_features,
"out_features": args.out_features,
"iters": args.iters,
"warmup": args.warmup,
"threads": (
os.environ.get("ORBITQUANT_CPU_THREADS", "runtime default")
if args.device == "cpu"
else None
),
"torch_threads": torch.get_num_threads() if args.device == "cpu" else None,
"with_bias": args.with_bias,
"packed_seconds_per_iter": packed_seconds,
"packed_first_call_seconds": packed_first_call_seconds,
"packed_hot_mean_seconds": packed_distribution["mean"],
"packed_hot_median_seconds": packed_distribution["median"],
"packed_hot_p95_seconds": packed_distribution["p95"],
"predequantized_f_linear_seconds_per_iter": predequantized_linear_seconds,
"predequantized_first_call_seconds": predequantized_first_call_seconds,
"predequantized_hot_mean_seconds": predequantized_distribution["mean"],
"predequantized_hot_median_seconds": predequantized_distribution["median"],
"predequantized_hot_p95_seconds": predequantized_distribution["p95"],
"dequantize_then_f_linear_seconds_per_iter": dequantize_then_linear_seconds,
"dequantize_then_first_call_seconds": dequantize_then_first_call_seconds,
"dequantize_then_hot_mean_seconds": dequantize_then_distribution["mean"],
"dequantize_then_hot_median_seconds": dequantize_then_distribution["median"],
"dequantize_then_hot_p95_seconds": dequantize_then_distribution["p95"],
"packed_weight_indices_bytes": packed_weight_indices_bytes,
"row_norms_bytes": row_norms_bytes,
"centroid_bytes": centroid_bytes,
"packed_weight_path_bytes": packed_weight_path_bytes,
"materialized_weight_bytes": materialized_weight_bytes,
"packed_weight_path_vs_materialized_weight_ratio": packed_weight_path_bytes
/ materialized_weight_bytes
if materialized_weight_bytes > 0
else None,
"packed_vs_predequantized_f_linear_speedup": predequantized_linear_seconds
/ packed_seconds
if packed_seconds > 0
else None,
"packed_vs_dequantize_then_f_linear_speedup": dequantize_then_linear_seconds
/ packed_seconds
if packed_seconds > 0
else None,
"reference_seconds_per_iter": predequantized_linear_seconds,
"packed_vs_reference_speedup": predequantized_linear_seconds / packed_seconds
if packed_seconds > 0
else None,
"max_abs_error": max_abs_error,
"rmse": rmse,
"relative_rmse": relative_rmse,
"timing_headline": "hot-loop median seconds per iteration",
"reference": (
"predequantized PyTorch F.linear over a materialized dequantized "
"weight matrix"
),
"dequantize_reference": (
"materialize the dequantized weight matrix, then call PyTorch F.linear"
),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--device", choices=["cpu", "cuda", "mps"], default="cuda")
parser.add_argument("--bits", type=int, default=4)
parser.add_argument(
"--rows",
type=_parse_rows,
default=[1, 8, 512, 4096],
help="comma-separated row counts to sweep (default covers decode-bound "
"small batches and GEMM-bound large batches)",
)
parser.add_argument(
"--dtype",
choices=["auto", *sorted(_DTYPES)],
default="auto",
help="activation dtype; auto picks float16 on mps and bfloat16 elsewhere",
)
parser.add_argument("--in-features", type=int, default=3072)
parser.add_argument("--out-features", type=int, default=3072)
parser.add_argument("--iters", type=int, default=20)
parser.add_argument("--warmup", type=int, default=3)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--threads", type=int, default=0)
parser.add_argument("--with-bias", action="store_true")
args = parser.parse_args()
if args.threads < 0:
parser.error("--threads must be non-negative")
if args.iters <= 0 or args.warmup < 0:
parser.error("--iters must be positive and --warmup must be non-negative")
if args.device == "cpu" and args.threads > 0:
os.environ["ORBITQUANT_CPU_THREADS"] = str(args.threads)
torch.set_num_threads(args.threads)
torch.manual_seed(args.seed)
if args.dtype == "auto":
dtype = torch.float16 if args.device == "mps" else torch.bfloat16
else:
dtype = _DTYPES[args.dtype]
indices = torch.randint(
0,
2**args.bits,
(args.out_features, args.in_features),
dtype=torch.uint8,
)
packed = _pack(indices, args.bits).to(args.device)
row_norms = torch.ones(args.out_features, device=args.device, dtype=torch.bfloat16)
centroids = torch.linspace(-1.0, 1.0, 2**args.bits, device=args.device)
indices_device = indices.long().to(args.device)
weights = (packed, row_norms, centroids, indices_device)
payloads = [_benchmark_rows(args, rows, dtype, weights) for rows in args.rows]
if len(payloads) == 1:
print(json.dumps(payloads[0], indent=2, sort_keys=True))
else:
print(json.dumps(payloads, indent=2, sort_keys=True))
if __name__ == "__main__":
main()