《互联网产品的 AI 架构·推理性能与成本工程》6.1 量化的精度与延迟权衡

8 位量化在 24 条样本上把 P95 延迟降到 82.2%、成本降到 70.0%,平均质量只降 0.0299,但把财务抽取加进验收集后同一档位立刻被拒。本节给出配对评测、P05 与灾难性样本、能力准入规则、灰度回滚条件,并说明哪些能力根本不能量化。

一次“省了三成”的评审

「回声」的推理账单连续两个月上涨。团队提出把一条非核心能力从 16 位降到 8 位:压测里 P95 从 1160ms 降到 953ms,单次成本大约省三成。评审材料只有一张表:平均质量分从 0.93 降到 0.90,结论是“可接受”。

这个结论跳过了三个问题。第一,0.90 是哪些样本的平均值,长尾掉得有多深。第二,哪些能力根本不允许降到 8 位。第三,验收样本里有没有覆盖金额、单号这类逐字符事实。量化不是把模型调暗一点,它是在用一个更粗的数值表示换取延迟与成本,代价必须逐条测出来。

本节只讨论三个档位:不量化、8 位、4 位。文中所有延迟、成本与质量数字都标注为「示例样本上的运行结果」,由确定性 mock 产生,不是任何厂商的量化参数,也不是生产实测。

量化改的不只是位宽

档位位宽延迟系数显存系数质量惩罚倍率
none161.001.000
w880.820.551.0
w440.680.352.2

三个系数回答三个不同的问题。延迟系数决定交互是否更快,显存系数决定能不能装进同一张卡或同一台机器,质量惩罚倍率决定精度损失的量级。它们不能互相替代:延迟降了 18%,不代表吞吐也降 18%,更不代表质量只降 18%。

精度损失必须配对测量

量化前后不能用两批样本各算一个平均分再相减。两批样本难度不同,差值里混进了抽样误差。正确做法是同一批样本、同一提示、同一评分器,逐条记录未量化分与量化分,再对差值做统计。

指标定义回答的问题
平均下降每条样本差值取均值整体让了多少
P05 下降第 5 百分位质量分的差长尾掉得多深
最差单条最大单条下降有没有极端个例
灾难性比例单条下降 ≥ 0.12 的占比会不会出现不可用输出
关键样本失败关键样本是否灾难性下降业务能不能接受

平均值最容易骗人。24 条样本平均只降 0.0299,看起来温和;但 P05 降了 0.0424,最差一条降了 0.0640,说明损失集中在少数难样本上。如果这些难样本正好是退款、合规或代码缺陷,平均值再好看也不能上线。

哪些能力不能量化

能力敏感度最高档位理由
search.rerankloww4排序偏好可容忍小幅扰动
chat.replymediumw4自然语言允许措辞差异
doc.summarizemediumw4摘要允许改述,事实必须存活
code.reviewhighw8缺陷定位依赖稳定推理
finance.extracthighnone金额、单号必须逐字符一致
safety.classifyhighnone安全误判不可逆

准入规则不是“平均分达标就放行”,而是先按能力判断档位是否允许。财务抽取和安全分类直接禁止量化;代码评审最多到 8 位;只有排序、闲聊与摘要可以尝试 4 位。未登记的新能力默认禁止,逼着团队显式给出理由,而不是继承一个宽松默认值。

验收集怎么建

示例用 24 条样本:4 个可量化能力各 6 条,分数、延迟与成本都可由索引复算。另加 6 条财务抽取作为“准入探针”,它不参与主验收,只用来验证能力规则真的会拦截。

样本量门槛写成常量:总数至少 20 条,每个能力至少 4 条。低于门槛时结论不是“通过”,而是“证据不足”。验收还必须包含关键样本;关键样本一旦灾难性下降,即使平均值达标也拒绝。

示例样本上的运行结果

8 位量化在 24 条可量化样本上的结果:

指标结果
未量化平均分0.9308
量化后平均分0.9009
平均下降0.0299
P05 下降0.0424
最差单条下降0.0640
灾难性比例0
P95 延迟1160ms → 953ms(0.822)
总成本13440 → 9412 微元(0.700)
结论通过

分能力看,排序降 0.0125、闲聊降 0.0244、摘要降 0.0260、代码评审降 0.0568。越依赖长链推理,损失越明显。代码评审虽然没有灾难性样本,但已经逼近 0.08 的单条容差。

同一档位加入 6 条财务抽取后,平均下降变成 0.0344,指标本身仍在阈值内,但结论被能力规则直接拒绝:finance.extract 最高只允许 none。这正是能力准入的价值:它不让“平均值还行”覆盖“这个能力不能碰”。

4 位量化的结果更直接:平均下降 0.0658,P05 下降 0.1053,灾难性比例 0.125,代码评审 6 条里 3 条越线;P95 延迟虽然降到 0.682,成本降到 0.450,仍被四条理由同时拒绝。

实现

模块包含档位表、能力规则、逐条配对评估、能力聚合与验收决策。它不访问网络,也不调用模型;输入是一批已经打好分的样本。

// file: src/perf/compress.ts
// tier: L2
// verify: node --test test/perf/compress.test.ts

import { AiError } from "../core/errors.ts";
import { percentile } from "../observe/baseline.ts";

/** 档位只描述位宽与相对开销,不绑定任何厂商的量化格式。 */
export const QUANT_LEVELS = ["none", "w8", "w4"] as const;
export type QuantLevel = (typeof QUANT_LEVELS)[number];

export interface QuantProfile {
  level: QuantLevel; bits: number; latencyFactor: number; memoryFactor: number;
  /** 质量惩罚倍率:0 表示不量化。 */
  penaltyScale: number;
}

export const QUANT_PROFILES: Record<QuantLevel, QuantProfile> = {
  none: { level: "none", bits: 16, latencyFactor: 1, memoryFactor: 1, penaltyScale: 0 },
  w8: { level: "w8", bits: 8, latencyFactor: 0.82, memoryFactor: 0.55, penaltyScale: 1 },
  w4: { level: "w4", bits: 4, latencyFactor: 0.68, memoryFactor: 0.35, penaltyScale: 2.2 },
};

export type Sensitivity = "low" | "medium" | "high";

export interface CapabilityQuantRule {
  capability: string;
  sensitivity: Sensitivity;
  /** 允许的最激进档位;none 表示该能力禁止量化。 */
  maxLevel: QuantLevel;
  reason: string;
}

export const DEFAULT_QUANT_RULES: Record<string, CapabilityQuantRule> = {
  "search.rerank": { capability: "search.rerank", sensitivity: "low", maxLevel: "w4", reason: "排序偏好可容忍小幅扰动" },
  "chat.reply": { capability: "chat.reply", sensitivity: "medium", maxLevel: "w4", reason: "自然语言回答允许措辞差异" },
  "doc.summarize": { capability: "doc.summarize", sensitivity: "medium", maxLevel: "w4", reason: "摘要允许改述,事实必须存活" },
  "code.review": { capability: "code.review", sensitivity: "high", maxLevel: "w8", reason: "缺陷定位依赖稳定推理" },
  "finance.extract": { capability: "finance.extract", sensitivity: "high", maxLevel: "none", reason: "金额、单号必须逐字符一致" },
  "safety.classify": { capability: "safety.classify", sensitivity: "high", maxLevel: "none", reason: "安全误判不可逆" },
};

const PENALTY: Record<Sensitivity, number> = { low: 0.012, medium: 0.026, high: 0.055 };
const RANK: Record<QuantLevel, number> = { none: 0, w8: 1, w4: 2 };

export interface QuantQualitySample {
  id: string;
  capability: string;
  /** 未量化时的质量分,0 到 1,来自同一套离线评测。 */
  baselineScore: number;
  baselineLatencyMs: number;
  baselineCostMicros: number;
  critical?: boolean;
}

export interface QuantSampleResult {
  id: string; capability: string; critical: boolean;
  baselineScore: number; quantizedScore: number; drop: number;
  baselineLatencyMs: number; quantizedLatencyMs: number;
  baselineCostMicros: number; quantizedCostMicros: number; catastrophic: boolean;
}

export interface CapabilityQuantSummary {
  capability: string; samples: number; baselineMean: number;
  quantizedMean: number; meanDrop: number; catastrophic: number;
}

export interface BlockedCapability { capability: string; maxLevel: QuantLevel; reason: string }

export interface QuantAcceptancePolicy {
  minSamples: number; minSamplesPerCapability: number; maxMeanDrop: number;
  maxP05Drop: number; maxCatastrophicRate: number; maxLatencyP95Ratio: number; maxCostRatio: number;
}

export const DEFAULT_ACCEPTANCE: QuantAcceptancePolicy = {
  minSamples: 20,
  minSamplesPerCapability: 4,
  maxMeanDrop: 0.04,
  maxP05Drop: 0.08,
  maxCatastrophicRate: 0.01,
  maxLatencyP95Ratio: 0.9,
  maxCostRatio: 0.8,
};

export interface QuantEvaluation {
  level: QuantLevel; profile: QuantProfile; samples: QuantSampleResult[];
  baselineMean: number; quantizedMean: number; meanDrop: number;
  baselineP05: number; quantizedP05: number; p05Drop: number; worstDrop: number;
  catastrophicRate: number; baselineP95LatencyMs: number; p95LatencyMs: number; latencyP95Ratio: number;
  baselineCostMicros: number; quantizedCostMicros: number; costRatio: number;
  byCapability: Record<string, CapabilityQuantSummary>;
  blockedCapabilities: BlockedCapability[];
  decision: { accepted: boolean; reasons: string[] };
}

function fail(message: string): never {
  throw new AiError("invalid-request", `compress: ${message}`);
}

function fnv1a(text: string): number {
  let hash = 0x811c9dc5;
  for (let index = 0; index < text.length; index += 1) {
    hash ^= text.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return hash;
}

function mean(values: readonly number[]): number {
  return values.length === 0 ? 0 : values.reduce((sum, value) => sum + value, 0) / values.length;
}

/** 未登记的能力默认禁止量化:新能力必须显式给规则,而不是继承宽松默认值。 */
export function ruleFor(capability: string): CapabilityQuantRule {
  return DEFAULT_QUANT_RULES[capability] ?? {
    capability, sensitivity: "high", maxLevel: "none", reason: "未登记的能力默认禁止量化",
  };
}

export function checkQuantizable(capability: string, level: QuantLevel): { allowed: boolean; reason: string } {
  const rule = ruleFor(capability);
  const allowed = RANK[level] <= RANK[rule.maxLevel];
  return {
    allowed,
    reason: allowed
      ? `${capability} 允许 ${level}:${rule.reason}`
      : `${capability} 最高只允许 ${rule.maxLevel},目标 ${level} 被拒绝:${rule.reason}`,
  };
}

function validate(sample: QuantQualitySample): void {
  if (sample.id.trim() === "" || sample.capability.trim() === "") fail("样本 id 与 capability 不能为空");
  if (!(sample.baselineScore >= 0 && sample.baselineScore <= 1)) fail(`样本 ${sample.id} 的 baselineScore 必须落在 [0, 1]`);
  if (!Number.isFinite(sample.baselineLatencyMs) || sample.baselineLatencyMs <= 0) fail(`样本 ${sample.id} 的延迟必须是正数`);
  if (!Number.isInteger(sample.baselineCostMicros) || sample.baselineCostMicros < 0) fail(`样本 ${sample.id} 的成本必须是非负整数`);
}

/** 质量损失用确定性模型模拟:能力敏感度 × 档位倍率 × 样本噪声。 */
function simulate(sample: QuantQualitySample, profile: QuantProfile): QuantSampleResult {
  const rule = ruleFor(sample.capability);
  const noise = 0.8 + (fnv1a(`${sample.id}:quality`) % 401) / 1000;
  const penalty = PENALTY[rule.sensitivity] * profile.penaltyScale * noise;
  const quantizedScore = Math.min(1, Math.max(0, sample.baselineScore - penalty));
  const quantizedLatencyMs =
    Math.round(sample.baselineLatencyMs * profile.latencyFactor) + (fnv1a(`${sample.id}:latency`) % 5);
  const costFactor = profile.level === "w4" ? 0.45 : profile.level === "w8" ? 0.7 : 1;
  const drop = sample.baselineScore - quantizedScore;
  return {
    id: sample.id,
    capability: sample.capability,
    critical: sample.critical === true,
    baselineScore: sample.baselineScore,
    quantizedScore,
    drop,
    baselineLatencyMs: sample.baselineLatencyMs,
    quantizedLatencyMs,
    baselineCostMicros: sample.baselineCostMicros,
    quantizedCostMicros: Math.round(sample.baselineCostMicros * costFactor),
    catastrophic: drop >= 0.12,
  };
}

function decide(
  data: Omit<QuantEvaluation, "decision" | "blockedCapabilities">,
  policy: QuantAcceptancePolicy,
  blocked: readonly BlockedCapability[],
): QuantEvaluation["decision"] {
  const reasons: string[] = [];
  if (blocked.length > 0) reasons.push(`能力不允许该档位:${blocked.map((item) => item.capability).join(",")}`);
  if (data.samples.length < policy.minSamples) reasons.push(`样本不足:${data.samples.length}/${policy.minSamples}`);
  for (const summary of Object.values(data.byCapability)) {
    if (summary.samples < policy.minSamplesPerCapability) {
      reasons.push(`能力 ${summary.capability} 样本不足:${summary.samples}/${policy.minSamplesPerCapability}`);
    }
  }
  if (data.meanDrop > policy.maxMeanDrop) reasons.push(`平均质量下降 ${data.meanDrop.toFixed(4)} > ${policy.maxMeanDrop}`);
  if (data.p05Drop > policy.maxP05Drop) reasons.push(`P05 质量下降 ${data.p05Drop.toFixed(4)} > ${policy.maxP05Drop}`);
  if (data.catastrophicRate > policy.maxCatastrophicRate) {
    reasons.push(`灾难性样本比例 ${data.catastrophicRate.toFixed(4)} > ${policy.maxCatastrophicRate}`);
  }
  if (data.latencyP95Ratio > policy.maxLatencyP95Ratio) {
    reasons.push(`P95 延迟比例 ${data.latencyP95Ratio.toFixed(3)} > ${policy.maxLatencyP95Ratio}`);
  }
  if (data.costRatio > policy.maxCostRatio) reasons.push(`成本比例 ${data.costRatio.toFixed(3)} > ${policy.maxCostRatio}`);
  if (data.samples.some((sample) => sample.critical && sample.catastrophic)) {
    reasons.push("关键样本出现灾难性质量下降");
  }
  return { accepted: reasons.length === 0, reasons };
}

/** 同一批样本配对评估:每条样本同时有未量化分与量化分,差值可逐条复算。 */
export function evaluateQuantization(
  samples: readonly QuantQualitySample[],
  level: QuantLevel,
  policy: QuantAcceptancePolicy = DEFAULT_ACCEPTANCE,
): QuantEvaluation {
  if (samples.length === 0) fail("至少需要一条评测样本");
  const profile = QUANT_PROFILES[level];
  if (profile === undefined) fail(`未知量化档位:${String(level)}`);
  for (const sample of samples) validate(sample);

  const blockedMap = new Map<string, BlockedCapability>();
  for (const sample of samples) {
    const rule = ruleFor(sample.capability);
    if (RANK[level] > RANK[rule.maxLevel]) {
      blockedMap.set(sample.capability, { capability: sample.capability, maxLevel: rule.maxLevel, reason: rule.reason });
    }
  }
  const results = samples.map((sample) => simulate(sample, profile));

  const groups = new Map<string, QuantSampleResult[]>();
  for (const result of results) {
    const bucket = groups.get(result.capability) ?? [];
    bucket.push(result);
    groups.set(result.capability, bucket);
  }
  const byCapability: Record<string, CapabilityQuantSummary> = {};
  for (const [capability, bucket] of groups) {
    const baselineMean = mean(bucket.map((item) => item.baselineScore));
    const quantizedMean = mean(bucket.map((item) => item.quantizedScore));
    byCapability[capability] = {
      capability, samples: bucket.length, baselineMean, quantizedMean,
      meanDrop: baselineMean - quantizedMean,
      catastrophic: bucket.filter((item) => item.catastrophic).length,
    };
  }

  const baselineScores = results.map((item) => item.baselineScore);
  const quantizedScores = results.map((item) => item.quantizedScore);
  const baselineP05 = percentile(baselineScores, 0.05);
  const quantizedP05 = percentile(quantizedScores, 0.05);
  const baselineP95LatencyMs = percentile(results.map((item) => item.baselineLatencyMs), 0.95);
  const p95LatencyMs = percentile(results.map((item) => item.quantizedLatencyMs), 0.95);
  const baselineCostMicros = results.reduce((sum, item) => sum + item.baselineCostMicros, 0);
  const quantizedCostMicros = results.reduce((sum, item) => sum + item.quantizedCostMicros, 0);
  const baselineMean = mean(baselineScores);
  const quantizedMean = mean(quantizedScores);
  const catastrophic = results.filter((item) => item.catastrophic).length;

  const data = {
    level, profile, samples: results, baselineMean, quantizedMean,
    meanDrop: baselineMean - quantizedMean,
    baselineP05, quantizedP05, p05Drop: baselineP05 - quantizedP05,
    worstDrop: Math.max(...results.map((item) => item.drop)),
    catastrophicRate: catastrophic / results.length,
    baselineP95LatencyMs, p95LatencyMs,
    latencyP95Ratio: p95LatencyMs / baselineP95LatencyMs,
    baselineCostMicros, quantizedCostMicros,
    costRatio: quantizedCostMicros / baselineCostMicros,
    byCapability,
  };
  return { ...data, blockedCapabilities: [...blockedMap.values()], decision: decide(data, policy, [...blockedMap.values()]) };
}

evaluateQuantization 先检查能力是否允许目标档位,再做配对模拟。被拦截的能力仍然会出现在 blockedCapabilities 里,方便评审看到“不是指标没过,是规则不许”。

drop 定义为未量化分减量化分,正数表示质量下降,避免“提升/下降”在不同函数里各有一套符号。P05 用最近秩法,和基线章节保持同一口径。灾难性阈值 0.12 是业务常量,不是统计显著性;团队可以调整,但必须和关键样本清单一起版本化。

测试与运行结果

测试覆盖 8 位通过、4 位拒绝、加入财务抽取后能力拦截、关键样本灾难性下降、能力规则、收紧阈值、重复评估一致性与空样本报错。执行:

node --test test/perf/compress.test.ts

本机一次运行的真实输出摘要如下,耗时随机器变化:

ℹ tests 8
ℹ pass 8
ℹ fail 0

测试没有断言“8 位一定好”,而是断言同一份样本上的具体差值、P05、灾难性比例与成本比例。这样换一台机器、换一版 mock,结论是否变化会立刻暴露。

灰度顺序

第一步是影子流量:线上请求照常走原模型,同时把同一批输入送进量化候选,只记录配对分数,不影响用户。第二步按能力白名单放量,先排序与摘要,再闲聊,代码评审只到 8 位。第三步才是按租户放量,每个租户单独看 P05 与关键样本。

回滚条件要提前写:P05 下降超过 0.08、出现一条关键样本灾难性下降、或成本没有降到预期的 80% 以下,任何一条触发就回到上一档。灰度不是“上线后观察”,而是每一步都有可判定的退出条件。

小结

量化的取舍可以压缩成三个判断:目标档位在目标硬件上带来多少延迟与成本收益,同一批样本上的质量损失分布如何,这个能力是否允许量化。三者缺一不可。示例样本上,8 位在 24 条可量化样本上通过,但加入财务抽取后立刻被能力规则拒绝;4 位则连平均值与灾难性比例都过不了。

下一节讨论另一种替代:不改变数值精度,而是把教师模型的能力蒸馏到一个更小的学生模型,再决定哪些能力可以替换、哪些必须回退。

阅读导航:上一节:5.3 缓存失效与一致性 · 下一节:6.2 蒸馏与小模型替代 。

常见错误与边界

  • 用两批样本各算平均分再相减。差值里混进抽样误差,结论不可复算。
  • 只看平均值,不看 P05 与最差单条。损失集中在少数难样本时,平均值会掩盖风险。
  • 先上量化再补验收集。没有基线分数,事后无法证明损失大小。
  • 对逐字符事实使用概率性精度。金额、单号、日期没有“差不多对”。
  • 对安全分类做量化。误判不可逆,省下的成本抵不过一次事故。
  • 用未登记能力继承默认档位。新能力必须显式给出上限与理由。
  • 把灾难性阈值当成统计显著性。它是业务容差,需要和关键样本一起评审。
  • 把示例 mock 的数字写成生产实测。所有数字必须标注来源与样本。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查