Skip to content

67. Quantized Inference and Deployment | 量化推理与部署

难度: Hard | 环境: CPU-first | 标签: 量化压缩, 量化推理, 部署 | 目标人群: 项目决策练习者

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)


本节导读

本节要求你验证一个量化推理方案是否满足部署条件。先固定 workload、运行后端和误差阈值,再比较 baseline 与量化方案的延迟、吞吐、显存和输出误差,同时记录 kernel 支持与反量化开销。最终输出一份部署建议,并明确方案适用的模型和服务条件。 层级定位: 本项目主落在 L4,关注量化模型如何被加载、执行和服务;低比特 kernel 属于 L2,模型版本、灰度发布和集群资源治理属于 L5,不在本项目内混为同一个结论。 主责与复用边界: 本项目主责是量化 artifact 的加载、执行和部署判断;显存优化路线只复用权重 / KV Cache 的容量证据,推理性能路线复用 TTFT、TPOT 和吞吐口径,训练侧 QLoRA 不在本项目重复验证。

关键词: quantization, inference, deployment

量化推理部署决策流程


前置阅读

导语: 先把 W8A16、QLoRA、量化理论和基础推理对比看过,再进入量化部署项目会更容易判断压缩收益是否值得保留。

相关阅读

导语: 完成量化部署选型后,用 74 检查收益是否有 profiler 证据;如果需要观察请求级行为,再进入 70 的 serving 调度基准。

Step 1: 定义量化部署项目目标

先固定模型、tokenizer、workload、硬件和 backend,再只改变量化 artifact 或量化格式。实验目标由显存、速度、质量和兼容性四类指标共同决定;低 bit 本身不是部署结论。

先按 66 的 G0/G1/G2 方式划分实验组:

组别环境实验内容主要回答的问题
C0 量化机制CPU改变 bits、group size 和权重分布存储量、元数据和误差如何变化?
C1 决策模拟CPU输入 baseline / candidate 指标和误差预算什么条件下值得继续部署?
G0 浮点 baselineGPU/backend固定模型和 workload,运行 FP16/BF16当前硬件和 backend 的基线是什么?
G1 真实量化GPU/backend只改变一种量化格式或 artifact量化是否真的降低显存或延迟?
G2 格式/backend 对照GPU/backend比较多个已确认支持的格式或 backend收益是否来自量化本身且值得迁移?

C0/C1 是 CPU-first 主线;G0/G1 需要真实 GPU 和量化 artifact;G2 是可选扩展。没有真实 artifact 时,G1 只能报告链路检查,不能填写量化收益。量化 bit、dtype 和 backend 必须作为不同变量记录。

候选本项目要验证的机制最小真实证据
GPTQ校准后的权重量化与误差补偿GPTQ artifact 被目标 backend 加载,并确认量化执行路径
AWQ激活感知与敏感权重保护AWQ artifact 被目标 backend 加载,并记录校准与 kernel 信息
GGUF文件封装、格式兼容与部署加载GGUF artifact 被对应 GGUF backend 加载;不能沿用 GPTQ/AWQ 的启动参数

Step 2: 先确认 baseline 与量化口径合法

先跑通 FP16/BF16 baseline,再只替换量化 artifact 或量化格式。下面的表格用于检查两组实验是否真的同口径:

条件G0 baselineG1/G2 candidate
模型与 tokenizer固定版本与 G0 相同
workloadprompt、生成长度、batch、并发、cache policy与 G0 相同
运行环境GPU、driver、PyTorch/CUDA、backend 版本尽量相同
唯一变量FP16/BF16 浮点权重量化 artifact、格式或明确 backend
校准记录不适用split、样本数、最大长度、版本

Step 3: 用统一口径比较收益与代价

同时比较性能、显存、数值误差和任务质量;每个指标都要能追溯到同一组 workload 和结果文件。

指标组记录字段用来回答什么
性能latency / TTFT / TPOT / throughput是否更快,是否适合当前请求负载
容量peak VRAM / load status是否装得下,是否提高并发或上下文上限
质量error / task metric量化误差是否超过预算
兼容性format / kernel evidence / backend version收益是否来自可复核的执行路径

Step 4: 输出部署选型结论

按质量门槛、兼容性和性能收益输出 accept / tune / reject:

决策条件下一步
accept真实 artifact 加载成功、kernel/格式已确认、质量达标且性能或容量有收益保留配置并扩大 workload 回归
tune质量达标但收益不稳定,或校准、粒度、backend 仍需调整补采数据或调整单一变量
reject质量超预算、无法加载、kernel 不支持或没有可接受收益更换格式、backend 或回到 baseline

报告必须保留量化格式、粒度、校准信息、backend、硬件和完整 workload;没有真实加载和 kernel 证据时,只能报告链路检查。

Step 5:CPU 实验——量化账本与误差机制

上面的 Step 1-4 定义项目口径;下面用 CPU 代码实现量化账本、模拟耗时、指标比较和部署决策。真实量化、校准、任务评估与压测放在 Step 6。

python
import math
import time
from typing import Dict, List
python
# TODO 0:实现 per-group 量化、反量化,并统计存储与误差
def simulate_weight_quantization(weights: List[float], bits: int = 8, group_size: int = 2) -> Dict[str, float]:
    """用对称 per-group 量化模拟存储压缩和反量化误差。

    bits 决定有符号量化范围,group_size 决定每个 scale 覆盖的权重数;
    返回的是权重重构误差和存储估算,不是模型任务质量或真实 artifact 大小。
    """
    # 提示:每组 scale 使用该组最大绝对值;全零组要避免除零。
    #       量化误差按 reconstructed - original 计算,元数据也要计入 quantized_bytes。
    if bits < 2 or bits > 8 or group_size < 1 or not weights:
        raise ValueError('bits 应在 2-8 之间,group_size >= 1,weights 不能为空')
    qmax = (1 << (bits - 1)) - 1
    reconstructed, group_count = [], 0
    for start in range(0, len(weights), group_size):
        group = [float(value) for value in weights[start:start + group_size]]
        scale = max(abs(value) for value in group) / qmax
        scale = scale if scale > 0 else 1.0
        reconstructed.extend(max(-qmax, min(qmax, round(value / scale))) * scale for value in group)
        group_count += 1
    errors = [estimate - actual for estimate, actual in zip(reconstructed, weights)]
    max_abs_error = max(abs(error) for error in errors)
    mse = sum(error * error for error in errors) / len(errors)
    original_bytes = len(weights) * 4
    scale_dtype_bits = 16  # 本模拟按 FP16 scale 估算元数据;真实格式需按 artifact 记录。
    quantized_bytes = math.ceil((len(weights) * bits + group_count * scale_dtype_bits) / 8)
    return {
        'parameter_count': len(weights), 'bits': bits, 'group_size': group_size,
        'groups': group_count, 'original_bytes': original_bytes,
        'quantized_bytes': quantized_bytes, 'scale_dtype_bits': scale_dtype_bits,
        'compression_ratio': round(original_bytes / quantized_bytes, 4),
        'max_abs_error': round(max_abs_error, 8), 'mse': round(mse, 8),
    }


def benchmark_fn(fn, warmup=2, iters=5):
    """测量一个候选函数的 CPU 平均耗时,返回毫秒。

    该函数不执行 CUDA synchronize,也不代表量化 kernel 或 backend 延迟。
    """
    # ==========================================
    # TODO 1: 先做 warmup,再测量平均耗时
    # 提示:用 time.perf_counter() 记录起止时间;warmup 不计入平均值。
    #       iters 必须为正,返回单位统一为 ms,方便和 latency 对齐。
    # ==========================================
    for _ in range(warmup):
        fn()

    # start = ???
    for _ in range(iters):
        fn()
    # total = ???
    # avg_latency_ms = ???
    return avg_latency_ms


def summarize_quantized_result(base_metrics, quant_metrics):
    """统一比较 baseline 与量化候选的收益、资源和重构误差。

    两个输入必须来自同一 workload;error_delta 表示候选误差相对 baseline 的变化,
    不能把 max_abs_error / mse 直接解释成任务质量下降。
    """
    # ==========================================
    # TODO 2: 汇总 baseline / quantized 的核心指标差异
    # 提示:latency / vram / error 越低越好,throughput 越高越好。
    #       latency_delta、vram_delta 使用 baseline - quantized;throughput_delta 使用 quantized - baseline。
    # 正数表示 quantized 相比 baseline 有改善,error_delta 除外
    # ==========================================
    # latency_delta = ???
    # throughput_delta = ???
    # vram_delta = ???
    # error_delta = ???

    summary = {
        'latency_delta_ms': round(latency_delta, 2),
        'throughput_delta': round(throughput_delta, 2),
        'vram_delta_mb': round(vram_delta, 2),
        'error_delta': round(error_delta, 4),
        'latency_improved': latency_delta > 0,
        'throughput_improved': throughput_delta > 0,
        'vram_improved': vram_delta > 0,
        'error_within_budget': error_delta <= quant_metrics['error_budget'],
    }
    return summary


def format_deployment_report(quant_name, summary, recommendation):
    """把量化候选的收益、误差和部署建议整理成可读报告。

    报告必须同时展示四类指标和 decision;不能只保留压缩率或延迟。
    """
    # ==========================================
    # TODO 3: 生成量化部署报告
    # 提示:把 latency、throughput、VRAM、error 的变化和 recommendation 放在一起。
    #       量化报告中的 error 仍是模拟/重构指标,真实任务质量要另行评测。
    # ==========================================
    header = "| 指标 | 变化 | 判断 |"
    sep = "| --- | --- | --- |"
    rows = [
        # f"| latency | {summary['latency_delta_ms']} ms | {'改善' if summary['latency_improved'] else '未改善'} |",
        # f"| throughput | {summary['throughput_delta']} | {'改善' if summary['throughput_improved'] else '未改善'} |",
        # f"| VRAM | {summary['vram_delta_mb']} MB | {'改善' if summary['vram_improved'] else '未改善'} |",
        # f"| error | {summary['error_delta']} | {'满足预算' if summary['error_within_budget'] else '超出预算'} |",
    ]
    # conclusion = ???
    return "\n".join([f"量化方案:{quant_name}", header, sep] + rows + [conclusion])


def recommend_quantized_deployment(summary, min_latency_delta_ms=5.0, min_throughput_delta=5.0, min_vram_delta_mb=256.0):
    """根据收益、误差和预算约束输出量化部署建议。

    返回 decision、reason、next_action;阈值属于当前 workload,不是所有 backend 的 SLA。
    """
    # ==========================================
    # TODO 4: 输出部署决策
    # 规则:
    # - 延迟或吞吐有明显收益,VRAM 也改善,且误差在预算内:accept
    # - 误差在预算内,但收益还不够稳:tune
    # - 误差超预算,或收益不足:reject
    # 提示:先计算四个布尔变量,再按质量门槛优先的顺序返回决策。
    # ==========================================
    # strong_latency_gain = ???
    # strong_throughput_gain = ???
    # strong_vram_gain = ???
    # error_ok = ???
    # if ???:
    #     decision = ???
    #     reason = ???
    #     next_action = ???
    # elif ???:
    #     decision = ???
    #     reason = ???
    #     next_action = ???
    # else:
    #     decision = ???
    #     reason = ???
    #     next_action = ???
    # return {'decision': decision, 'reason': reason, 'next_action': next_action}

测试

python
def test_quantized_project_template():
    try:
        weights = [0.0, 1.0, -2.0, 3.0, 0.5]
        quant_report = simulate_weight_quantization(weights, bits=4, group_size=2)
        assert quant_report['parameter_count'] == 5
        assert quant_report['groups'] == 3
        assert quant_report['quantized_bytes'] < quant_report['original_bytes']
        assert quant_report['max_abs_error'] >= 0.0 and quant_report['mse'] >= 0.0
        try:
            simulate_weight_quantization(weights, bits=16)
        except ValueError:
            pass
        else:
            raise AssertionError('不支持的 bit 数应明确拒绝!')
        counter = {'n': 0}

        def fn():
            counter['n'] += 1

        avg = benchmark_fn(fn, warmup=0, iters=2)
        assert counter['n'] == 2, "benchmark 应该运行 iters 次"
        assert avg >= 0.0, "平均耗时应该非负"

        baseline = {
            'latency_ms': 100.0,
            'throughput': 80.0,
            'vram_mb': 12000.0,
            'error': 0.0,
        }
        quantized = {
            'latency_ms': 72.0,
            'throughput': 120.0,
            'vram_mb': 7000.0,
            'error': 0.012,
            'error_budget': 0.02,
        }
        summary = summarize_quantized_result(baseline, quantized)

        assert summary['latency_delta_ms'] == 28.0
        assert summary['throughput_delta'] == 40.0
        assert summary['vram_delta_mb'] == 5000.0
        assert summary['error_delta'] == 0.012
        assert summary['latency_improved'] is True
        assert summary['throughput_improved'] is True
        assert summary['vram_improved'] is True
        assert summary['error_within_budget'] is True

        decision = recommend_quantized_deployment(summary, min_latency_delta_ms=5.0, min_throughput_delta=5.0, min_vram_delta_mb=256.0)
        assert decision['decision'] == 'accept'
        assert decision['next_action'] == 'promote_to_extended_regression'

        weak_summary = dict(summary)
        weak_summary['latency_delta_ms'] = 1.0
        weak_summary['throughput_delta'] = 2.0
        weak_summary['vram_delta_mb'] = 128.0
        weak_summary['latency_improved'] = True
        weak_summary['throughput_improved'] = True
        weak_summary['vram_improved'] = True
        weak_decision = recommend_quantized_deployment(weak_summary, min_latency_delta_ms=5.0, min_throughput_delta=5.0, min_vram_delta_mb=256.0)
        assert weak_decision['decision'] == 'tune'

        bad_summary = dict(summary)
        bad_summary['error_within_budget'] = False
        bad_decision = recommend_quantized_deployment(bad_summary, min_latency_delta_ms=5.0, min_throughput_delta=5.0, min_vram_delta_mb=256.0)
        assert bad_decision['decision'] == 'reject'

        report = format_deployment_report('W8A16', summary, decision['reason'])
        assert 'W8A16' in report
        assert '| 指标 | 变化 | 判断 |' in report
        assert '值得推进到更大样本部署回归' in report

        print("✅ 量化推理与部署项目模板代码通过基础校验。")
    except NotImplementedError:
        print("请先完成 TODO 代码!")
        raise
    except (AttributeError, NameError, TypeError, ValueError) as e:
        print("代码可能未完成,导致变量未定义")
        raise NotImplementedError("请先完成 TODO 代码!") from e
    except AssertionError as e:
        print(f"❌ 测试失败: {e}")
        raise NotImplementedError("请先完成 TODO 代码!") from e


test_quantized_project_template()

🛑 STOP HERE 🛑









请先尝试自己完成代码并跑通测试。
如果你正在 Colab 中运行,并且遇到困难没有思路,可以向下滚动查看参考答案。










参考代码与解析

代码

python
# TODO 0: 参考实现:per-group 量化、反量化,以及存储/误差统计
def simulate_weight_quantization(weights: List[float], bits: int = 8, group_size: int = 2) -> Dict[str, float]:
    """用对称 per-group 量化模拟存储压缩和反量化误差。"""
    if bits < 2 or bits > 8 or group_size < 1 or not weights:
        raise ValueError('bits 应在 2-8 之间,group_size >= 1,weights 不能为空')
    qmax = (1 << (bits - 1)) - 1
    reconstructed, group_count = [], 0
    for start in range(0, len(weights), group_size):
        group = [float(value) for value in weights[start:start + group_size]]
        scale = max(abs(value) for value in group) / qmax
        scale = scale if scale > 0 else 1.0
        reconstructed.extend(max(-qmax, min(qmax, round(value / scale))) * scale for value in group)
        group_count += 1
    errors = [estimate - actual for estimate, actual in zip(reconstructed, weights)]
    max_abs_error = max(abs(error) for error in errors)
    mse = sum(error * error for error in errors) / len(errors)
    original_bytes = len(weights) * 4
    scale_dtype_bits = 16  # 本模拟按 FP16 scale 估算元数据;真实格式需按 artifact 记录。
    quantized_bytes = math.ceil((len(weights) * bits + group_count * scale_dtype_bits) / 8)
    return {
        'parameter_count': len(weights), 'bits': bits, 'group_size': group_size,
        'groups': group_count, 'original_bytes': original_bytes,
        'quantized_bytes': quantized_bytes, 'scale_dtype_bits': scale_dtype_bits,
        'compression_ratio': round(original_bytes / quantized_bytes, 4),
        'max_abs_error': round(max_abs_error, 8), 'mse': round(mse, 8),
    }


def benchmark_fn(fn, warmup=2, iters=5):
    # ==========================================
    # TODO 1: 先做 warmup,再测量平均耗时
    # 提示: 用 time.perf_counter() 记录起止时间
    # 返回单位统一为 ms,方便和 latency 对齐
    # ==========================================
    for _ in range(warmup):
        fn()

    start = time.perf_counter()
    for _ in range(iters):
        fn()
    total = time.perf_counter() - start
    avg_latency_ms = total / iters * 1000
    return avg_latency_ms


def summarize_quantized_result(base_metrics, quant_metrics):
    # ==========================================
    # TODO 2: 汇总 baseline / quantized 的核心指标差异
    # 提示: latency / vram / error 越低越好,throughput 越高越好
    # 正数表示 quantized 相比 baseline 有改善,error_delta 除外
    # ==========================================
    latency_delta = base_metrics['latency_ms'] - quant_metrics['latency_ms']
    throughput_delta = quant_metrics['throughput'] - base_metrics['throughput']
    vram_delta = base_metrics['vram_mb'] - quant_metrics['vram_mb']
    error_delta = quant_metrics['error'] - base_metrics['error']

    summary = {
        'latency_delta_ms': round(latency_delta, 2),
        'throughput_delta': round(throughput_delta, 2),
        'vram_delta_mb': round(vram_delta, 2),
        'error_delta': round(error_delta, 4),
        'latency_improved': latency_delta > 0,
        'throughput_improved': throughput_delta > 0,
        'vram_improved': vram_delta > 0,
        'error_within_budget': error_delta <= quant_metrics['error_budget'],
    }
    return summary


def format_deployment_report(quant_name, summary, recommendation):
    # ==========================================
    # TODO 3: 生成量化部署报告
    # 提示: 把指标变化、误差约束和部署建议放在一起
    # ==========================================
    header = "| 指标 | 变化 | 判断 |"
    sep = "| --- | --- | --- |"
    rows = [
        f"| latency | {summary['latency_delta_ms']} ms | {'改善' if summary['latency_improved'] else '未改善'} |",
        f"| throughput | {summary['throughput_delta']} | {'改善' if summary['throughput_improved'] else '未改善'} |",
        f"| VRAM | {summary['vram_delta_mb']} MB | {'改善' if summary['vram_improved'] else '未改善'} |",
        f"| error | {summary['error_delta']} | {'满足预算' if summary['error_within_budget'] else '超出预算'} |",
    ]
    conclusion = f"部署建议:{recommendation}。"
    return "\n".join([f"量化方案:{quant_name}", header, sep] + rows + [conclusion])


def recommend_quantized_deployment(summary, min_latency_delta_ms=5.0, min_throughput_delta=5.0, min_vram_delta_mb=256.0):
    strong_latency_gain = summary['latency_delta_ms'] >= min_latency_delta_ms
    strong_throughput_gain = summary['throughput_delta'] >= min_throughput_delta
    strong_vram_gain = summary['vram_delta_mb'] >= min_vram_delta_mb
    error_ok = summary['error_within_budget']

    if error_ok and (strong_latency_gain or strong_throughput_gain) and strong_vram_gain:
        decision = 'accept'
        reason = '收益和误差预算都达标,值得推进到更大样本部署回归。'
        next_action = 'promote_to_extended_regression'
    elif error_ok and (summary['latency_improved'] or summary['throughput_improved'] or summary['vram_improved']):
        decision = 'tune'
        reason = '误差仍在预算内,但收益还不够稳,先继续调量化粒度、校准集或后端。'
        next_action = 'refine_quant_granularity_or_backend'
    else:
        decision = 'reject'
        reason = '误差超预算,或收益不足以支撑部署切换。'
        next_action = 'fallback_to_baseline_or_rework_quant_scheme'
    return {'decision': decision, 'reason': reason, 'next_action': next_action}

解析

这页现在按 simulate -> measure -> compare -> report -> decide 的最小量化部署项目闭环组织,不再只是单独比较 bit 数或显存收益。

TODO 0

  • 实现方式:按 group 切分权重,用对称 scale 做量化和反量化,统计量化存储、元数据开销以及最大绝对误差 / MSE。
  • 关键点:压缩比不是简单的 32 / bits,还要计入每组 scale 等元数据;误差指标也不能直接等同于任务精度。
  • 项目意义:CPU 可以验证 bit、分组粒度、存储账本与误差之间的关系;真实低比特 kernel 的速度、workspace、显存峰值和部署兼容性仍需 GPU/backend 验证。

TODO 1

  • 实现方式:先做 warmup,再统计 iters 轮的总耗时,最后换算成单次平均 latency。
  • 关键点:返回值统一用 ms,方便后面和部署报告里的延迟指标直接对齐。
  • 项目意义:量化部署不是只看模型大小,先要把测量口径收平,后面才谈收益是否可信。

TODO 2

  • 实现方式:统一计算 latency_deltathroughput_deltavram_deltaerror_delta
  • 关键点:前三类指标正数表示改善,error_delta 是部署约束,不应该误读成性能收益。
  • 项目意义:这一步把量化方案从“压低 bit 数”转成“速度、显存和误差能否一起成立”的项目比较。

TODO 3

  • 实现方式:把 latency、throughput、VRAM、error 放到同一张表,再补一句部署建议。
  • 关键点:报告必须同时呈现收益和误差预算,避免只凭显存下降就直接上线。
  • 项目意义:量化部署项目最后要回答的不是“能不能量化”,而是“这套方案是否满足部署约束,下一轮该扩大回归还是继续校准”。

Step 6(可选):GPU/backend 实验——真实量化部署

GPU 量化实验流程

实验条件表

项目G0 baselineG1 量化候选G2 格式/backend 对照
模型与 workload固定与 G0 相同与 G0 相同
dtype / 量化格式FP16 或 BF16只改变一种真实量化格式明确列出各格式
calibration / evaluation不适用 / 独立评估记录数据集、split、样本数分别记录
backend / 硬件固定固定尽量固定

结果表模板

实验组formatprompt/outputbatch/concurrencyTTFT/latencythroughputpeak VRAMerror/task qualityload/kerneldecision
G0FP16/BF16固定固定待采集待采集待采集参考输出成功/待确认待判断
G1真实量化格式与 G0 相同与 G0 相同待采集待采集待采集待采集成功/待确认待判断

执行顺序:先运行 G0,确认浮点模型、backend 和固定 workload 能正常完成;再只替换量化 artifact 或量化启动参数运行 G1;最后把另一种格式或另一种 backend 作为 G2 单独重跑。每一组都要保留 load_statusformatkernel_evidenceTTFT/latencythroughputpeak_vramquality,然后再生成 accept / tune / reject

证据边界:CPU 模拟只能支持存储量和误差机制结论。只有真实量化权重被 backend 加载、目标 kernel 或格式得到确认,并在相同 workload 下测量延迟、吞吐、显存和质量,才能支持量化部署收益结论。

本节推荐的真实 GPU 数据口径是:Qwen2.5-1.5B-Instruct,WikiText-2 train 子集用于 calibration,validation 子集用于评估;0.5B 仅作为快速 smoke 档。 数据准备单元只下载独立 split 并保存口径清单;当前的长度限制是字符级近似,真正执行 GPTQ/AWQ 前必须使用目标 tokenizer 重新截断和打包。 按 66 的统一分组执行:先固定模型、tokenizer、workload、backend 和硬件,运行 G0 浮点 baseline;再只替换一份真实量化 artifact,运行 G1;最后才把 GPTQ、AWQ、GGUF 或不同 backend 分别作为 G2 对照。G0 与 G1 必须写入同一份配对报告,至少包含加载状态、格式、kernel 证据、延迟、吞吐、峰值显存和质量字段。只有真正加载量化权重或量化启动参数,才可记录量化收益;服务启动成功本身不等于量化收益成立。GGUF 继续使用独立 backend 路径,不与 vLLM 的 GPTQ/AWQ 启动参数混写。

默认保持 Practice-P1 的本地/模拟量化实验;需要接入 backend 时,将 RUN_REAL_BACKEND 改为 True。模型来源支持 automodelscopehuggingface 或本地目录,dtype 与端口由共享 helper 自动选择。当前 vLLM 路径会依次启动 G0 和 G1,并把两个结果合并保存;G2 需要修改 QUANTIZATION_FORMAT 或 backend 后另行运行。若目标格式或 backend 尚未接入专用启动参数,入口会主动停止,不能把 artifact 加载成功等同于量化收益成立。

Colab / ModelScope:先确保 Notebook 位于仓库根目录(或先 clone 仓库),再运行下面单元;没有 GPU 时保留 False,不会阻断前面的 CPU-first 练习。

python
import json
from pathlib import Path

try:
    from tools.inference_project_runtime import locate_repo_root
    REPO_ROOT = locate_repo_root()
    from tools.inference_project_runtime import (
        shared_project_config, save_project_result, start_optional_vllm,
        stop_optional_vllm, start_external_openai_backend, run_backend_benchmark,
    )
    from tools.backend_runtime import (
        probe_vllm_quantization_support, build_vllm_quantization_args, find_free_port,
    )
except ModuleNotFoundError:
    # 题目测试或纯 CPU 环境可能没有仓库工具;真实 backend 入口保持关闭。
    RUN_REAL_BACKEND = False
    def shared_project_config(**kwargs): return kwargs
    def save_project_result(*args, **kwargs): raise RuntimeError('需要从仓库根目录运行真实 backend 入口')

RUN_REAL_BACKEND = False  # 是否启动真实 backend;默认只练习量化决策模板。
MODEL_PROFILES = {
    'smoke': 'Qwen/Qwen2.5-0.5B-Instruct',
    'gpu_quant': 'Qwen/Qwen2.5-1.5B-Instruct',
}
MODEL_PROFILE = 'smoke'  # smoke 适合快速链路;gpu_quant 用于真实量化收益实验。
MODEL_ID = MODEL_PROFILES[MODEL_PROFILE]  # 量化前后必须保持同一基座模型。
MODEL_SOURCE = 'auto'  # 模型来源:auto / modelscope / huggingface / local。
MODEL_CACHE_DIR = 'model_cache'  # 模型缓存目录;相对路径从仓库根目录解析。
DTYPE = 'auto'  # 非量化计算 dtype;auto 根据当前 GPU 选择。
QUANTIZATION_FORMAT = 'none'  # none / gptq / awq / gguf;none 只表示浮点 baseline。
QUANTIZATION_BACKEND = 'vllm'  # 真实量化的执行后端;GGUF 不应直接沿用 vLLM 路径。
GGUF_COMMAND_TEMPLATE = None  # 例如 ['llama-server', '-m', '{model_path}', '--port', '{port}'];需由目标引擎确认。
RUN_DATA_PREP = False  # 是否下载并准备独立的 calibration/evaluation 文本。
CALIBRATION_DATASET = 'wikitext'  # Hugging Face 数据集名称。
DATASET_CONFIG = 'wikitext-2-raw-v1'  # WikiText-2 的配置名称。
CALIBRATION_SPLIT = 'train'  # 校准只使用 train 子集。
CALIBRATION_SAMPLES = 128  # Colab/小显存先用小子集,正式实验再扩大。
CALIBRATION_MAX_LENGTH = 512  # 校准样本截断长度。
EVAL_DATASET = 'wikitext'  # 与 calibration 分开的评估数据集名称。
EVAL_SPLIT = 'validation'  # 评估使用独立 split。
EVAL_SAMPLES = 128  # 先做固定小样本 perplexity/质量评估。
QUANTIZATION_ARTIFACT = None  # 真实量化权重或目录;None 时不能宣称量化收益。
KERNEL_EVIDENCE = 'pending_manual_confirmation'  # backend 启动不等于目标量化 kernel 已确认。
BATCH_SIZE = 1  # baseline 与量化候选必须保持一致。
CONCURRENCY = 1  # 只在单独的并发实验中修改。
NUM_PROMPTS = 5  # smoke 请求数;正式实验应扩大并重复。
MAX_TOKENS = 64  # 每个请求的生成上限。
WARMUP = 1  # 不计入正式统计的预热请求数。
MAX_MODEL_LEN = 2048  # backend 的上下文上限,影响 KV Cache 预算。
BACKEND = 'vllm'  # 推理运行时;更换 backend 会改变 kernel 支持范围。
CACHE_POLICY = 'default'  # KV Cache 策略;对照实验中应固定。
RESULT_PATH = 'benchmarks/results/67_quantized_deployment.json'  # 统一结果文件。
DATA_MANIFEST_PATH = 'benchmarks/results/67_quantization_data_manifest.json'  # 只保存数据口径,不保存数据正文。

project_config = shared_project_config(
    model=MODEL_ID, backend=BACKEND, dtype=DTYPE,
    quantization_format=QUANTIZATION_FORMAT, quantization_backend=QUANTIZATION_BACKEND,
    quantization_artifact=QUANTIZATION_ARTIFACT,
    kernel_evidence=KERNEL_EVIDENCE,
    calibration_split=CALIBRATION_SPLIT, calibration_samples=CALIBRATION_SAMPLES,
    calibration_max_length=CALIBRATION_MAX_LENGTH, eval_dataset=EVAL_DATASET,
    eval_split=EVAL_SPLIT, eval_samples=EVAL_SAMPLES,
    generated_tokens=MAX_TOKENS, batch=BATCH_SIZE, concurrency=CONCURRENCY,
    cache_policy=CACHE_POLICY,
)
print(project_config)

def validate_quantization_setup():
    """阻止把普通浮点服务误报成真实量化实验。"""
    valid_formats = {'none', 'gptq', 'awq', 'gguf'}
    if QUANTIZATION_FORMAT not in valid_formats:
        raise ValueError(f'QUANTIZATION_FORMAT 必须是 {sorted(valid_formats)} 之一。')
    if not str(QUANTIZATION_BACKEND).strip():
        raise ValueError('QUANTIZATION_BACKEND 不能为空;请明确记录实际执行后端。')
    if QUANTIZATION_FORMAT == 'none' and QUANTIZATION_ARTIFACT is not None:
        raise ValueError('QUANTIZATION_FORMAT=none 时不能填写 QUANTIZATION_ARTIFACT。')
    if QUANTIZATION_FORMAT != 'none' and not QUANTIZATION_ARTIFACT:
        raise ValueError('真实量化实验必须提供 QUANTIZATION_ARTIFACT。')
    if QUANTIZATION_ARTIFACT and not Path(QUANTIZATION_ARTIFACT).exists():
        raise FileNotFoundError(f'量化 artifact 不存在:{QUANTIZATION_ARTIFACT}')
    if QUANTIZATION_FORMAT == 'gguf' and QUANTIZATION_BACKEND == 'vllm':
        raise ValueError('GGUF 必须使用已确认支持 GGUF 的独立 backend,不能直接沿用 vLLM 启动路径。')
    if QUANTIZATION_FORMAT != 'none' and QUANTIZATION_BACKEND != BACKEND:
        raise ValueError('量化 backend 与服务 backend 不一致;请先拆成独立实验,避免把格式切换和服务栈切换混为一个变量。')
    if QUANTIZATION_FORMAT in {'gptq', 'awq'} and QUANTIZATION_BACKEND not in {'vllm', 'sglang', 'transformers'}:
        raise ValueError('GPTQ/AWQ 的 backend 需先登记为 vllm、sglang 或 transformers,并确认实际支持。')
    if MODEL_PROFILE == 'gpu_quant' and QUANTIZATION_FORMAT == 'none':
        print('提示:gpu_quant 当前仍是浮点 baseline;请设置真实量化格式后再运行 G1。')
    return {
        'artifact_required': QUANTIZATION_FORMAT != 'none',
        'artifact_configured': bool(QUANTIZATION_ARTIFACT),
        'format': QUANTIZATION_FORMAT,
        'backend': QUANTIZATION_BACKEND,
        'same_as_serving_backend': QUANTIZATION_BACKEND == BACKEND,
    }

def inspect_quantization_artifact() -> dict:
    """检查 artifact 的基本格式,不把检查结果当作 kernel 兼容性证明。"""
    if QUANTIZATION_FORMAT == 'none':
        return {'status': 'baseline', 'format': 'none'}
    artifact = Path(QUANTIZATION_ARTIFACT)
    if QUANTIZATION_FORMAT == 'gguf':
        files = [artifact] if artifact.is_file() else sorted(artifact.glob('*.gguf'))
        if not files:
            raise ValueError('GGUF artifact 必须是 .gguf 文件,或包含 .gguf 文件的目录。')
        return {'status': 'metadata_present', 'format': 'gguf', 'files': [str(item) for item in files]}
    config_path = artifact / 'config.json' if artifact.is_dir() else artifact.parent / 'config.json'
    if not config_path.exists():
        raise ValueError(f'{QUANTIZATION_FORMAT.upper()} artifact 缺少 config.json,无法确认格式元数据。')
    metadata = json.loads(config_path.read_text(encoding='utf-8'))
    quant_config = metadata.get('quantization_config') or metadata.get('quantization')
    if not isinstance(quant_config, dict):
        raise ValueError(f'{QUANTIZATION_FORMAT.upper()} artifact 未发现 quantization_config 元数据。')
    declared = json.dumps(quant_config, ensure_ascii=False).lower()
    if QUANTIZATION_FORMAT not in declared:
        raise ValueError(f'artifact 元数据未声明 {QUANTIZATION_FORMAT.upper()},请不要把普通浮点目录当作量化模型。')
    return {'status': 'metadata_present', 'format': QUANTIZATION_FORMAT, 'config_path': str(config_path), 'quantization_config': quant_config}

QUANTIZATION_SETUP = validate_quantization_setup()
ARTIFACT_INSPECTION = inspect_quantization_artifact()
project_config['artifact_inspection'] = ARTIFACT_INSPECTION
if RUN_REAL_BACKEND and QUANTIZATION_FORMAT == 'gguf' and not GGUF_COMMAND_TEMPLATE:
    raise ValueError('GGUF 实验必须提供包含 {model_path}{port} 的 GGUF_COMMAND_TEMPLATE。')

QUANTIZATION_LAUNCH_ARGS = None
if RUN_REAL_BACKEND and QUANTIZATION_FORMAT in {'gptq', 'awq'}:
    quant_capability = probe_vllm_quantization_support()
    QUANTIZATION_LAUNCH_ARGS = build_vllm_quantization_args(quant_capability, QUANTIZATION_FORMAT)

# 量化候选完成本地测量后,用下面的调用保存统一结果:
# save_project_result(RESULT_PATH, project='67', strategy='w8a16',
#     config=project_config, metrics=metrics, quality=quality, decision=decision)

def load_calibration_and_eval_texts():
    """加载独立 split 的小型文本子集;不执行量化,也不替代任务评估。"""
    try:
        from datasets import load_dataset
    except ImportError as exc:
        raise RuntimeError('数据准备需要 datasets,请先安装 requirements 中的依赖。') from exc
    calibration = load_dataset(
        CALIBRATION_DATASET, DATASET_CONFIG,
        split=f'{CALIBRATION_SPLIT}[:{CALIBRATION_SAMPLES}]',
    )
    evaluation = load_dataset(
        EVAL_DATASET, DATASET_CONFIG,
        split=f'{EVAL_SPLIT}[:{EVAL_SAMPLES}]',
    )
    max_chars = CALIBRATION_MAX_LENGTH * 4  # 这里只做近似字符截断;正式量化前仍需 tokenizer 截断。
    calibration_texts = [row['text'].strip()[:max_chars] for row in calibration if row.get('text', '').strip()]
    evaluation_texts = [row['text'].strip()[:max_chars] for row in evaluation if row.get('text', '').strip()]
    if not calibration_texts or not evaluation_texts:
        raise ValueError('calibration/evaluation split 没有可用文本。')
    return {'calibration': calibration_texts, 'evaluation': evaluation_texts}

if RUN_DATA_PREP:
    DATASET_BUNDLE = load_calibration_and_eval_texts()
    manifest = {
        'dataset': CALIBRATION_DATASET, 'config': DATASET_CONFIG,
        'calibration_split': CALIBRATION_SPLIT,
        'calibration_samples': len(DATASET_BUNDLE['calibration']),
        'calibration_max_length': CALIBRATION_MAX_LENGTH,
        'evaluation_split': EVAL_SPLIT,
        'evaluation_samples': len(DATASET_BUNDLE['evaluation']),
    }
    manifest_path = Path(DATA_MANIFEST_PATH)
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
    print(manifest)
    print(f'数据口径清单已保存:{manifest_path}')
else:
    print('跳过数据下载:RUN_DATA_PREP=False;当前仅打印量化实验配置。')

# 真实 backend 先验证服务链路;量化格式专用启动参数需要按实际引擎补充。
if RUN_REAL_BACKEND and QUANTIZATION_FORMAT == 'gguf':
    from tools.model_runtime import resolve_model
    serving_model = QUANTIZATION_ARTIFACT
    model_path = resolve_model(serving_model, MODEL_SOURCE, cache_dir=MODEL_CACHE_DIR)
    port = find_free_port()
    server, log_path = start_external_openai_backend(
        GGUF_COMMAND_TEMPLATE, model_path=str(model_path), port=port,
        log_path='benchmarks/results/67_gguf_backend.log',
    )
    try:
        report = run_backend_benchmark(
            project='67', base_url=f'http://127.0.0.1:{port}', model=MODEL_ID,
            label='gguf-deployment-smoke', output=RESULT_PATH, backend=QUANTIZATION_BACKEND,
            dtype=DTYPE, cache_policy=CACHE_POLICY, batch=BATCH_SIZE,
            concurrency=CONCURRENCY, num_prompts=NUM_PROMPTS, max_tokens=MAX_TOKENS,
            warmup=WARMUP,
        )
        print(report['normalized_result'])
    finally:
        stop_optional_vllm(server, log_path)

def run_vllm_candidate(label, model_id, model_source, quantization_args, output_path):
    """启动一个候选、运行固定 workload,并返回可与另一候选配对的报告。

    G0 与 G1 只允许改变模型 artifact / 量化启动参数;服务名、dtype、
    上下文长度和请求配置沿用同一组配置。函数不把服务启动成功解释为
    量化收益,kernel 和任务质量仍需在报告中单独确认。
    """
    server, log_path, port, selected_dtype, model_path = start_optional_vllm(
        model_id=model_id, model_source=model_source, dtype=DTYPE,
        max_model_len=MAX_MODEL_LEN, served_model_name=MODEL_ID,
        quantization_args=quantization_args,
    )
    try:
        report = run_backend_benchmark(
            project='67', base_url=f'http://127.0.0.1:{port}', model=MODEL_ID,
            label=label, output=output_path, backend=BACKEND,
            dtype=selected_dtype, cache_policy=CACHE_POLICY,
            batch=BATCH_SIZE, concurrency=CONCURRENCY, num_prompts=NUM_PROMPTS,
            max_tokens=MAX_TOKENS, warmup=WARMUP,
        )
        return {
            'label': label, 'model_path': str(model_path),
            'dtype': selected_dtype, 'port': port,
            'report_path': str(output_path),
            'load_status': 'backend_started_and_benchmark_completed',
            'kernel_evidence': KERNEL_EVIDENCE,
            'normalized_result': report.get('normalized_result', report),
        }
    finally:
        stop_optional_vllm(server, log_path)


if RUN_REAL_BACKEND and QUANTIZATION_FORMAT != 'gguf':
    if BACKEND != 'vllm':
        raise RuntimeError('当前 G0/G1 自动配对入口使用 vLLM;SGLang 请沿用独立 backend 入口并保持相同报告字段。')
    if QUANTIZATION_FORMAT == 'none':
        raise ValueError('配对实验需要 QUANTIZATION_FORMAT=gptq 或 awq;none 只运行 CPU/配置检查。')

    result_root = Path(RESULT_PATH)
    baseline_path = result_root.with_name(f'{result_root.stem}_g0_baseline.json')
    candidate_path = result_root.with_name(f'{result_root.stem}_g1_{QUANTIZATION_FORMAT}.json')
    baseline = run_vllm_candidate(
        'g0-float-baseline', MODEL_ID, MODEL_SOURCE, None, baseline_path,
    )
    candidate = run_vllm_candidate(
        f'g1-{QUANTIZATION_FORMAT}', QUANTIZATION_ARTIFACT, 'local',
        QUANTIZATION_LAUNCH_ARGS, candidate_path,
    )
    paired_result = {
        'schema_version': 'quantized-inference-project/v1',
        'project': '67_quantized_inference_and_deployment',
        'stage': 'matched_backend_measurement',
        'config': project_config,
        'comparison': {
            'fixed': ['model_tokenizer', 'workload', 'backend', 'hardware', 'dtype_policy', 'cache_policy'],
            'changed': ['quantization_artifact', 'quantization_format', 'quantization_launch_args'],
        },
        'baseline': baseline,
        'candidate': candidate,
        'quality': {
            'status': 'pending_task_evaluation',
            'note': 'backend benchmark 已完成;需补充相同输入的输出质量或 perplexity 后才能判定部署接受。',
        },
        'evidence_checks': {
            'baseline_and_candidate_backend_started': True,
            'quantization_format_metadata': ARTIFACT_INSPECTION.get('status'),
            'kernel_evidence': KERNEL_EVIDENCE,
            'task_quality': 'pending',
        },
        'decision': {
            'decision': 'tune',
            'reason': 'G0/G1 已完成同口径 backend 测量,但质量与 kernel 证据仍需人工核对。',
            'next_action': 'verify_kernel_and_task_quality_then_compare_metrics',
        },
        'evidence_level': 'matched_backend_measurement_pending_quality',
    }
    result_root.parent.mkdir(parents=True, exist_ok=True)
    result_root.write_text(json.dumps(paired_result, ensure_ascii=False, indent=2), encoding='utf-8')
    print(json.dumps(paired_result, ensure_ascii=False, indent=2))

Released under the MIT License.