小菜鸟

java菜鸟号正在起航

大模型量化实战(下):从敲命令到验精度

上篇我们聊了量化的本质是“误差甩锅”,也给了选型决策表。但很多读者反馈:“知道该选 AWQ 了,可打开终端还是不知道敲什么。”“量化完怎么验证?PPL 跑完了然后呢?”

这篇就是来填坑的。我们将聚焦两件事:怎么用 ms-swift 一行命令完成量化,以及怎么科学验证量化模型没变傻。不再有“精密博弈”之类的废话,直接上命令、上脚本、上诊断思路。

💡 本篇定位
这是上篇的“实操续集”。如果你还没读过上篇,建议先花 5 分钟了解“误差甩锅”和选型逻辑,否则本篇的命令会显得孤立。

1. 量化实操:ms-swift 能做什么、不能做什么

ms-swift 是国内最友好的量化工具,但它不是万能的。先明确边界,再谈命令:

方法 ms-swift 支持 替代方案 说明
AWQ-W4A16 --quant_method awq - 首选推荐,中文校准集友好
GPTQ-W4A16 --quant_method gptq - HF 原生兼容性好
FP8-W8A8 --quant_method fp8 - 支持 H100/Ada/Mi300X 等 FP8 硬件加速;W8A8 中精度通常优于 INT8,但与 W4A16 各有胜负
BNB-NF4 --quant_method bnb - 4090 可完成 72B 量化导出(导出峰值显存高);推理仍需 ≥48G 显存,4090 推理建议选更小模型或启用 CPU offload
Ollama 格式 --to_ollama - 一键导出 Ollama 可直接加载的格式,本地试玩首选
SmoothQuant-W8A8 不支持 AutoGPTQ / TensorRT-LLM ms-swift 无 PTQ/QAT 入口
GGUF 不支持导出 llama.cpp quantize 需用 llama.cpp 独立转换

💡 关键提醒
上篇提到的 SmoothQuant 和 GGUF,在 ms-swift 中无法一键完成。如果你需要这两种方案,请跳转到对应工具链。本篇聚焦 ms-swift 真实支持的四种量化方法及 Ollama 导出。

最小可运行命令(复制即用)

阅读全文 »

大模型量化实战(上):从显存焦虑到算法本质

想在本地跑个 70B 的大模型?FP16 精度下光权重就得吃掉 140G 显存,连加载都报错。这时候你肯定盯上了量化:换成 INT4,35G 显存搞定,一张 RTX 4090 就能起飞。

但真用起来,坑就来了:

  • 明明体积缩小了 4 倍,生成速度怎么没快 4 倍?
  • 评测分数看着没掉,怎么让它写个复杂代码就开始胡言乱语了?
  • 网上满天飞的 GPTQ、AWQ、W8A8,到底该下哪个包?

很多人以为量化就是“把文件压缩一下”,其实根本不是。它更像是在给大桥换便宜的钢材 ——有些钢缆是主承重墙,换差了桥就塌了;有些只是栏杆,随便换。

而且别被 PPL(困惑度)这种评测指标骗了。很多时候分数没变,但模型的长尾知识召回下降、多步推理链断裂、格式化输出不稳定 ——这些才是量化最常见的“隐形退化”。这就是为什么不能闭着眼睛无脑转 INT4。

为了搞懂这些,我把大模型里需要量化的东西分成了三块,它们的脾气完全不一样:

要量化的东西 通常压到多少 难在哪 实际影响
权重 (Weights) INT4/INT8 存在少量 outlier 通道(通常占 <1%,但绝对数量随模型增大可达几十至上百个) 省显存的绝对主力,离线弄好就行
激活值 (Activations) INT8/FP8 经常冒出极大值(Outlier),动态变化 决定推理能不能真正提速,必须在线处理
KV Cache INT4/INT8/FP8 聊得越久占得越多 长文本场景下的救命稻草

这篇文章不打算给你背公式。我们直接从这几个实际问题出发,用大白话盘一盘主流算法到底是怎么解决这些“刺头”的,最后给你一张直接能用的选型决策表。

阅读全文 »

SciPy 与 SymPy:数值算结果,符号推公式,各管各的

做科学计算的时候,你经常会遇到两种需求:

  • 算一个具体的数:比如 ∫₀¹ sin(x) dx = 0.4596...
  • 推一个数学公式:比如 ∫ x·sin(x) dx = -x·cos(x) + sin(x)

前者是数值计算(SciPy 的活),后者是符号运算(SymPy 的活)。数值给结果,符号给表达式。 两套工具,解决两类问题。

数值积分:函数没原函数,或者数据来自实验

很多工程问题需要算积分,但被积函数可能没有初等原函数,或者数据是测量来的(离散点,没有表达式)。这时候用数值积分。

一维定积分:quad(最常用)

from scipy.integrate import quad
import numpy as np

# 算 ∫₀¹ sin(x) dx
result, error = quad(np.sin, 0, 1)
print(f"{result:.10f}")   # 0.4596976941

quad 返回两个值:积分结果和误差估计。误差估计别看小,它告诉你结果可不可信。 如果误差比结果还大,说明积分没收敛。

带参数的积分:

def integrand(x, a, b):
    return np.exp(-a * x) * np.sin(b * x)

result, error = quad(integrand, 0, np.inf, args=(2.0, 3.0))

二重积分:dblquad(注意积分顺序)

from scipy.integrate import dblquad

# 算半球体积:∫∫ √(1-x²-y²) dy dx
def f(x, y):
    return np.sqrt(max(0, 1 - x**2 - y**2))

volume, error = dblquad(
    f,
    -1, 1,
    lambda x: -np.sqrt(1-x**2),
    lambda x: np.sqrt(1-x**2)
)
print(f"{volume:.6f}")   # 2.094395(= 2π/3)
阅读全文 »

SciPy 数值优化:从最小二乘到非线性求解,遇到什么问题该用什么

写代码做数据处理,很多问题最终都能归结成一个核心需求:找到一组最优的参数

  • 拟合一条曲线 → 找参数使误差最小
  • 控制一个系统 → 找参数使性能最好
  • 解一个方程组 → 找一组值使所有等式成立

SciPy 的 optimize 模块就是干这个的。但问题是:算法太多了——leastsqcurve_fitfminfmin_bfgsfsolve……新手看了直接懵。


最小二乘拟合:数据点 + 模型 = 找参数

90% 的优化需求都能归到这一类:你有数据点,你有一个带参数的模型,你想让模型尽可能贴合数据。

标准写法(老派):

from scipy.optimize import leastsq
import numpy as np

x = np.array([1, 2, 3, 4, 5])
y = 2.5 * x + 1.3 + np.random.randn(5) * 0.5

def residuals(p, x, y):
    k, b = p
    return y - (k * x + b)

p0 = [1.0, 0.0]
result = leastsq(residuals, p0, args=(x, y))
k, b = result[0]
print(f"k={k:.3f}, b={b:.3f}")

更友好的写法(推荐):

from scipy.optimize import curve_fit

def model(x, k, b):
    return k * x + b

popt, pcov = curve_fit(model, x, y, p0=[1.0, 0.0])
k, b = popt

curve_fitleastsq 好用得多——不用自己写残差函数,不用操心 args 传参方式,直接定义模型函数就行。新代码优先用 curve_fit

拟合正弦波(复杂模型示例):

阅读全文 »

NumPy 核心原理:搞懂内存结构和广播,性能问题不用猜

用 NumPy 的人很多,但遇到性能问题或奇怪报错的时候,大多数人是靠试——换个写法试试,不行再换。靠猜也能解决问题,但下次遇到类似问题还是懵。

搞懂 ndarray 的内存结构和广播机制,能让你从”靠猜”变成”靠判断”。

  • 为什么 a[1:3, 2:5] 这种切片几乎不花时间?
  • 为什么修改切片有时候会影响原数组,有时候不影响?
  • 为什么同样的运算,某些写法快几十倍?

答案都在 ndarray 的内存模型里。

ndarray 长什么样?数据 + 说明书

一个 ndarray 对象在内存里就两样东西:

  1. 实际数据:一块连续的内存
  2. 元数据:描述怎么解读这块内存
┌─────────────────────────────────────────┐
│           ndarray 对象                   │
├─────────────────────────────────────────┤
│  data    → 数值数据(连续内存)          │
│  dtype   → 元素类型(int32 / float64)   │
│  shape   → 各轴长度 (3, 4)              │
│  strides → 各轴步长 (16, 4)             │
└─────────────────────────────────────────┘

关键认知:数据只存一份。 切片、转置、大部分 reshape 都是只改说明书(元数据),不改数据本身。

dtype:每个元素占多少字节

dtype 告诉你每个元素占几个字节、怎么解释这串字节。

import numpy as np

np.dtype('int32').itemsize   # 4 字节
np.dtype('float64').itemsize # 8 字节

当你从二进制文件(WAV 音频、BMP 图片)读数据时,dtype 必须跟文件格式匹配,否则读出来的数据完全错乱。这不是”报错”,而是”数据对不上但程序不吭声”,更难排查。

strides:步长,理解 NumPy 性能的钥匙

strides 记录了在某个轴上前进 1 步,内存里要跳过多少字节

a = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8]], dtype=np.float64)

print(a.strides)   # (32, 8)
  • strides[0] = 32:行前进 1 步 = 4 个元素 × 8 字节 = 32 字节
  • strides[1] = 8:列前进 1 步 = 1 个元素 × 8 字节 = 8 字节

切片 a[1:3, 1:3] 几乎不花时间——它只是新建了一个 ndarray 对象,改了 shapestrides数据没动

b = a[1:3, 1:3]
b.base is a   # True —— 共享同一块数据

转置也是改步长:

c = a.T
c.strides   # (8, 32) —— 步长交换了

reshape 不一定”免费”:

#  免费:数据在内存里是连续的
a.reshape(2, 6)

#  可能触发复制:转置后数据不连续
a.T.reshape(2, 6)   # 可能产生副本

因为 a.T 的数据在内存里不连续,没法只靠改元数据来 reshape。

内存布局:C 风格 vs Fortran 风格

# C 风格(行优先):最后一轴变化最快
c_style = np.arange(12).reshape(3, 4)

# Fortran 风格(列优先):第一轴变化最快
f_style = np.array(c_style, order='F')

调用底层库(特别是 LAPACK 这种 Fortran 写的)时,内存布局不匹配会触发隐式复制,拖慢性能。知道这个,就能在创建数组时直接指定 order,避免后面被动复制。

广播机制:不同形状也能运算

广播允许不同形状的数组做算术运算,不复制数据

a = np.array([[1, 2, 3],
              [4, 5, 6]])   # shape (2, 3)
b = np.array([10, 20, 30])  # shape (3,)

c = a + b   # 结果 (2, 3)
# b 被"广播"到每一行

广播的两条规则(背下来):

  1. 从最后一个维度开始往前比对
  2. 两个维度要么相等,要么其中一个是 1,要么不存在
# 可以广播
a.shape: (2, 3, 4)
b.shape:    (3, 4)   # 4=4✓, 3=3✓, a 多出一个2 → 广播

#  无法广播
a.shape: (2, 3)
b.shape:    (4)      # 3≠4 ✗ → ValueError

广播的实际场景

场景1:标准化数据

data = np.random.rand(100, 50)
mean = data.mean(axis=0)   # shape (50,)
std = data.std(axis=0)     # shape (50,)
normalized = (data - mean) / std   # 广播自动处理

场景2:外积

x = np.array([1, 2, 3])   # (3,)
y = np.array([4, 5, 6])   # (3,)
outer = x[:, np.newaxis] * y   # (3,1) × (3,) → (3,3)

场景3:生成网格(画 3D 图常用)

X, Y = np.meshgrid(
    np.linspace(-1, 1, 100),
    np.linspace(-1, 1, 100)
)
Z = np.sin(X**2 + Y**2)

广播 vs 手动扩展:内存差异巨大

a = np.random.rand(10000, 100)
b = np.random.rand(100)

#  广播:不复制数据
result1 = a + b

#  手动扩展:多占了 10000*100*8 字节
b_expanded = np.tile(b, (10000, 1))
result2 = a + b_expanded

两者结果一样,但广播的内存效率高得多。数据量小看不出来,数据量大的时候直接决定程序能不能跑得动。

0%