> [!NOTE]
> 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
> [English](./README.en.md) · [原始项目](https://github.com/Mininglamp-AI/cider) · [上游 README](https://github.com/Mininglamp-AI/cider/blob/HEAD/README.md)
> 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
# cider
Cider 基于 MLX 为 macOS 开发。它提供:
1. MLX 所不具备的**在线激活量化**(W8A8/W4A8),配有自定义 INT8 TensorOps 内核,作为 MLX 自定义原语(custom primitives)构建,支持完整的惰性求值(lazy evaluation)。
2. 受 [FlashInfer](https://github.com/flashinfer-ai/flashinfer), 启发的**优化 SDPA 解码内核**,采用连续块 GQA 调度与寄存器分块(register tiling),以实现更快的自回归解码。
它还包含服务端扩展以及对 mlx_vlm 的非侵入式兼容性补丁(已在 mlx_vlm 0.4.3 上验证),包括修复与 RoPE 位置处理及分块预填充(chunked prefill)相关的 Qwen3-VL 多图推理问题。
## 条件编译(M4 / M5)
Cider 采用**条件编译**:INT8 TensorOps C++ 扩展仅在 Apple M5+ 上构建。
| Chip | `pip install -e .` behavior | `import cider` behavior |
|------|----------------------------|------------------------|
| **M5+** | 完整构建(CMake + Metal kernels) | 全部功能可用 |
| **M4 及以下** | 跳过 C++ 构建,安装纯 Python 包 | `is_available()` → False,`convert_model()` 为警告空操作(warning no-op) |
**通过环境变量覆盖:**
```bash
CIDER_FORCE_BUILD=1 pip install -e . # Force build (e.g., CI)
CIDER_FORCE_BUILD=0 pip install -e . # Force skip
```
## 模式
| Mode | Weights | Activations | Compute Path | Status |
|------|---------|-------------|--------------|--------|
| **W8A8** | INT8 symmetric | INT8 per-token | TensorOps matmul2d | ✅ 已实现 |
| **W4A8** | INT4 packed (uint8) | INT8 per-token | Unpack→TensorOps | ✅ 已实现 |
| W4A16 | — | — | MLX built-in | 基线 |
| W8A16 | — | — | MLX built-in | 基线 |
**W4A16 和 W8A16 已由 MLX 原生支持** — 本 SDK 提供 MLX 未实现的 W8A8 与 W4A8 模式。
## 优化 SDPA(解码注意力)
Cider 包含面向自回归解码(`Q_seq=1`)的优化**缩放点积注意力**(Scaled Dot-Product Attention)内核,受 [FlashInfer](https://github.com/flashinfer-ai/flashinfer). 启发。关键技术:
- **连续块 GQA 调度(Contiguous Chunk GQA Scheduling)**:不再让每个 Q 头独立派发(读取 KV `H_q` 次),而是由单个线程组处理共享同一 KV 组的所有 Q 头。这可减少最多 `gqa_factor`× 的 DRAM 带宽。
- **寄存器分块(Register Tiling,TILE=4)**:每个线程在寄存器中保存 4 个 Q 头的部分结果 — 无需共享内存,无需屏障同步 — 最大化分组查询解码的 ALU 吞吐。
- **两遍 FlashDecoding(2-Pass FlashDecoding)**:对于长序列(≥1024 tokens),两遍归约将 KV 序列划分到各线程组,以提升占用率。
- **AutoTune**:针对(架构、GQA 比率、头数、序列长度)自动扫描块大小,并缓存至 `~/.cider_sdpa_tune.json`。
### 用法
```python
import cider
# One-line patch — all downstream frameworks (mlx_lm, mlx_vlm) benefit automatically
cider.patch_sdpa()
# Optional: run AutoTune once to find optimal block sizes for your chip
cider.autotune_sdpa()
# Undo patch if needed
cider.unpatch_sdpa()
```
该补丁**完全透明**:预填充(`Q_seq > 1`)与带掩码注意力会自动回退到 MLX 原生实现。仅解码步骤(`Q_seq=1`,无掩码)会走 Cider 的优化内核。**无需修改** mlx_lm 或 mlx_vlm 代码。
### SDPA 微基准(Apple M5 Pro,D=128)
相对 MLX `mx.fast.scaled_dot_product_attention` 的加速比:
| Config | N=1K | N=2K | N=4K | N=8K | N=16K | N=32K |
|--------|------|------|------|------|-------|-------|
| MHA (H_q=32, H_kv=32) | 1.03x | 1.01x | 1.02x | 1.00x | 1.00x | 0.99x |
| GQA4 (H_q=32, H_kv=8) | 1.04x | 1.06x | 1.06x | 1.12x | 1.16x | 1.21x |
| GQA8 (H_q=32, H_kv=4) | 1.04x | 1.07x | 1.13x | 1.21x | 1.34x | **1.57x** |
加速范围:**0.99×–1.57×**。正确性:18/18 PASS(相对 MLX 最大差异 ≤ 0.000122,fp16)。14 WIN / 4 TIE / 0 LOSS — 零回退。
### 启用 SDPA 的端到端解码速度(Qwen3-VL-4B,Apple M5 Pro)
使用 `benchmarks/bench_e2e_wxa16.py` 在真实多图 VLM 轨迹上测试(1–3 张截图,1334–3455 prompt tokens):
| Config | Decode tok/s | vs Baseline |
|--------|:-----------:|:-----------:|
| FP16 | 32.0 | — |
| FP16 + Cider SDPA | 32.6 | +1.9% |
| W8A16 | 54.0 | — |
| W8A16 + Cider SDPA | **58.0** | **+7.4%** |
| W8A8 per-group | 51.0 | — |
| W8A8 per-group + Cider SDPA | 52.3 | +2.5% |
| W8A8 per-channel | 49.1 | — |
| W8A8 per-channel + Cider SDPA | **52.6** | **+7.1%** |
Cider SDPA 可叠加在 W8A8 预填充收益之上。收益随 GQA 比率与 KV cache 长度而放大 — GQA 比率更高(例如 GQA8 的 Qwen3-32B)且上下文更长的模型获益更大。
MLX 的量化是**仅权重量化(weight-only)**:QuantizedLinear 将权重反量化为 FP16 并使用 FP16 GEMM。虽然 MLX 的 Steel NAX 模板足够通用,可实例化为 INT8 类型(且能达到相同的原始 matmul 吞吐 — [参见我们的透明基准测试](benchmarks/mlx_native/cider_vs_mlx_int8.md)),但 MLX 并未提供实际 W8A8 推理所需的量化/反量化流水线。Cider 通过融合的 quantize-matmul-dequant 原语填补这一缺口。
本 SDK 为受支持的推理路径实现在线 INT8 激活量化,以及基于 INT8 TensorOps 的计算。
### W8A8 量化粒度
| Granularity | Description | Speed | Precision |
|-------------|-------------|-------|-----------|
| **Per-channel** | 每个输出通道一个 scale | 最快(预填充 1.8x) | 略低 |
| **Per-group (gs=128)** | 每 128 个元素一个 scale | 较快(预填充 1.5x) | 精度保留适中 |
| **Per-group (gs=64)** | 每 64 个元素一个 scale | 中等(预填充 1.3x) | 精度更高 |
## 性能(Apple M5 Pro)
### 单算子延迟
Shape [N=10240, K=2560]
| M | PC(ms) | PG(ms) | w8a16 | w4a16 | PC/w8 | PC/w4 | PG/w8 | PG/w4|
|-----|------|------|-----|-----|-----|------|-----|----|
| 1 | 0.27ms | 0.26ms | 0.26ms | 0.18ms | 0.96x | 0.67x | 0.99x | 0.69x |
|128 | 0.34ms | 0.39ms | 0.49ms | 0.44ms | 1.43x | 1.28x | 1.26x | 1.13x |
|1024 | 1.23ms | 1.52ms | 2.24ms | 2.04ms | 1.82x | 1.66x | 1.47x | 1.34x|
|4096 | 4.41ms | 5.65ms | 8.12ms | 7.72ms | 1.84x | 1.75x | 1.44x | 1.37x |
|8192 | 8.71ms | 11.40ms | 16.23ms | 15.09ms | 1.86x | 1.73x | 1.42x | 1.32x|
Shape [N=2560, K=10240]
| M | PC(ms) | PG(ms) | w8a16 | w4a16 | PC/w8 | PC/w4 | PG/w8 | PG/w4 |
|--------|------|--------|-------| ---|--------|------|-------------|------------------|
| 1 | 0.25ms | 0.26ms | 0.26ms | 0.20ms | 1.03x | 0.78x | 0.98x | 0.75x |
|128 | 0.39ms | 0.41ms | 0.55ms | 0.46ms | 1.43x | 1.19x | 1.35x | 1.12x |
| 1024 | 1.31ms | 1.65ms | 2.35ms | 2.14ms | 1.80x | 1.64x | 1.43x | 1.30x |
| 4096 | 5.37ms | 6.79ms | 8.54ms | 8.04ms | 1.59x | 1.50x | 1.26x | 1.18x |
| 8192 | 10.97ms | 12.94ms | 17.28ms | 16.23ms | 1.58x | 1.48x | 1.34x | 1.25x |
### 端到端 VLM
**Qwen3-VL-2B**
| Prompt Tokens | FP16 Prefill (tok/s) | W8A16 Prefill (tok/s) | **W8A8 PC Prefill (tok/s)** | FP16 Decode (tok/s) | W8A16 Decode (tok/s) | **W8A8 PC Decode (tok/s)** |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1334 | 3010 | 2065 | **3242** | 70 | 107 | **104** |
| 2393 | 2868 | 1847 | **2983** | 69 | 97 | **100** |
| 3455 | 2777 | 1741 | **2796** | 66 | 90 | **95** |
**Qwen3-VL-4B**
Prompt Tokens | FP16 Prefill (tok/s) | W8A16 Prefill (tok/s) | **W8A8 PC Prefill (tok/s)** | FP16 Decode (tok/s) | W8A16 Decode (tok/s) | **W8A8 PC Decode (tok/s)** |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1334 | 1884 | 1786 | **2186** | 32 | **56** | 54 |
| 2393 | 1815 | 1700 | **2028** | 31 | **55** | 52 |
| 3455 | 1755 | 1603 | **1881** | 30 | **52** | 49 |
### LLM 量化:精度与速度对比
| Models |
Quantization Configuration |
wikitext2 PPL(↓) |
Prefill Time (s)(↓) |
Peak Memory (GB)(↓) |
| Qwen3-8B |
FP16 |
9.726 |
179.9 |
18.93 |
| W8A16 (mlx RTN) |
9.707 |
221.3 |
12.07 |
| W8A8 (per-channel) |
9.756 |
123.5 |
11.32 |
| W8A8 (per-group gs=64) |
9.744 |
179.1 |
11.83 |
| W8A8 (per-group gs=128) |
9.727 |
165.8 |
11.61 |
|
| Llama3-8B |
FP16 |
6.138 |
175.8 |
18.32 |
| W8A16 (mlx RTN) |
6.147 |
236.9 |
11.46 |
| W8A8 (per-channel) |
6.271 |
123.3 |
10.69 |
| W8A8 (per-group, gs=64) |
6.269 |
178.7 |
11.19 |
| W8A8 (per-group, gs=128) |
6.270 |
155.7 |
10.98 |
## 说明
需要注意的是,cider 假定权重是 **量化友好(quantization-friendly)** 的。这意味着你需要确保模型已经经过某种训练后量化校准方法,例如 GTPQ、SmoothQuant 或 Quarot 等,以处理异常值(outliers),或者模型本身是通过 QAT(量化感知训练)训练的。当 cider 转换模型(例如使用 w8a8)并计算量化 scale 时,默认采用最简单的 **min-max** 方法,同时也支持第 99 百分位方法(clip_percentile=99.9)。如果遇到乱码输出,很大概率是模型受到了 **异常值(outliers)** 的影响。因此,某些预处理步骤是 **必要的**。
## 环境要求
- **Apple M5+** 用于 INT8 TensorOps(M4 及以下:以纯 Python 方式安装,`is_available()` 返回 False)
- Python 3.12+
- MLX >= 0.31
- nanobind >= 2.12(仅在 M5+ 上进行 C++ 编译时需要)
- CMake >= 3.27(仅在 M5+ 上进行 C++ 编译时需要)
## 安装
```bash
pip install -e .
```
在 M5+ 上,这会运行 CMake 编译 C++ 扩展,然后安装 Python 包。
在 M4 及以下设备上,仅安装 Python 包(不会出现编译错误)。
## 快速开始
### 一行命令完成模型转换(推荐)
```python
from cider import convert_model, is_available
model, proc = load("path/to/model")
if is_available():
convert_model(model)
# CiderLinear auto-detects:
# seq_len > 1 → W8A8 INT8 TensorOps (faster prefill)
# seq_len == 1 → INT8 MV kernel (near-native decode speed)
else:
pass # Falls back to standard MLX inference on M4
```
**重要提示**
在量化视觉语言模型(Vision-Language Models,VLMs)时,视觉 Transformer(ViT)通常不会被替换。直接使用 convert_model 也会量化视觉模型的线性层,这通常会导致精度下降。对于 VLMs,我们建议调用 convert_model(model.language_model),仅对语言模型应用 GPTQ、SmoothQuant 和 AWQ 等现有量化方法。
已在选定的 MLX Transformer 模型上测试,包括 Qwen3、Qwen3-VL 和 Llama3 系列。其他架构可能需要适配。
### 层级别 API
```python
import numpy as np
import mlx.core as mx
from cider import W8A8Linear, W4A8Linear, is_available
assert is_available(), "Requires Apple M5+"
# Prepare weight
W = np.random.randn(4096, 4096).astype(np.float16)
# W8A8 linear (per-channel)
from cider.ops import quantize_weight_int8
w_int8, scale = quantize_weight_int8(W)
layer = W8A8Linear(
w_int8=mx.array(w_int8), scale_w=mx.array(scale),
group_size=0, in_features=4096, out_features=4096
)
x = mx.random.normal((32, 4096)).astype(mx.float16)
y = layer(x) # lazy — builds MLX graph
mx.eval(y) # GPU executes
# W4A8 linear (half the weight memory)
layer4 = W4A8Linear.from_weights(W)
y4 = layer4(x)
mx.eval(y4)
```
## 底层 API
```python
from cider import perchannel_linear, w4a8_linear, quantize_weight_int8, pack_weight_int4
# Quantize weights (numpy, offline)
w_int8, scale = quantize_weight_int8(W_np)
packed_w4, scale4 = pack_weight_int4(W_np)
# Primitive calls (return lazy mx.array)
y = perchannel_linear(x, mx.array(w_int8), mx.array(scale))
y4 = w4a8_linear(x, mx.array(packed_w4), mx.array(scale4))
```
## 项目结构
```
cider/
├── cider/ # Python package
│ ├── __init__.py # Public API (conditional on is_available)
│ ├── ops.py # Primitive wrappers + quantize helpers
│ ├── nn.py # CiderLinear, W4A8Linear (nn.Module)
│ ├── convert.py # convert_model() high-level API
│ ├── attention/ # Optimized SDPA
│ │ ├── __init__.py
│ │ └── sdpa.py # patch_sdpa / unpatch_sdpa / autotune_sdpa
│ └── kernels/ # Metal shaders (bundled)
│ ├── w8a8_matmul.metal # W8A8 GEMM (prefill, M>1)
│ ├── w8a8_int8_mv.metal # W8A8 per-channel MV (decode, M=1)
│ ├── w8a8_quantize.metal # Per-token activation quantization
│ ├── w4a8_matmul.metal # W4A8 GEMM (prefill)
│ ├── pergroup_int8_gemm.metal # Per-group GEMM (prefill)
│ ├── pergroup_int8_mv.metal # Per-group MV (decode)
│ ├── cider_sdpa_vector.h # SDPA kernel header (1-pass & 2-pass)
│ └── cider_sdpa_vector.metal # SDPA kernel template instantiations
├── csrc/ # C++ MLX primitives (nanobind, M5+ only)
│ ├── include/
│ │ ├── w8a8_primitive.h
│ │ ├── w4a8_primitive.h
│ │ ├── pergroup_primitive.h
│ │ └── sdpa_primitive.h
│ └── src/
│ ├── w8a8_primitive.mm
│ ├── w4a8_primitive.mm
│ ├── pergroup_primitive.mm
│ ├── sdpa_primitive.mm # SDPA 1-pass & 2-pass primitives
│ └── prim_bindings.cpp
├── benchmarks/
│ ├── bench_e2e_wxa16.py # End-to-end VLM benchmark (W8A8 + SDPA)
│ ├── bench_sdpa.py # SDPA micro-benchmark (correctness + speed vs MLX)
│ ├── bench_full.py # Isolated kernel latency (per-channel/per-group vs MLX)
│ ├── test_bitexact.py # Numerical correctness verification
│ └── mlx_native/ # MLX native INT8 comparison
├── tutorial/
│ ├── how_to_write_efficient_int_gemm_m5_en.md
│ └── how_to_write_efficient_int_gemm_m5_zh.md
├── tools/
│ ├── eval_ppl_all.py # Unified PPL eval (FP16/W8A16/per-channel/per-group)
│ ├── convert_compressed_tensors_to_mlx.py
│ └── smoothquant.py # SmoothQuant calibration
├── examples/
│ └── basic_usage.py
├── vlm_service/ # OpenAI-style VLM inference server
│ ├── server.py # FastAPI server (streaming + non-streaming)
│ ├── core_infer.py # HMInference engine (singleton)
│ ├── custom_qwen3vl.py # Custom Qwen3-VL generation loop
│ ├── config.py # Config loader
│ ├── bench_client.py # Server benchmark client
│ └── client.py # API client example
├── config/
│ └── config.yaml # Server & model configuration
├── experimental/ # ANE+GPU hybrid tensor parallelism (M4)
│ ├── split_linear.py # SplitLinear + ANEBridge + patch_model()
│ ├── bench.py # End-to-end benchmark
│ ├── libane_bridge_v6.m # ANE private API bridge (Obj-C source)
│ └── README.md
├── CMakeLists.txt
├── pyproject.toml
├── setup.py # Conditional build (M5+: full, M4: pure-Python)
└── README.md
```
## VLM 推理服务
`vlm_service/` 提供了一个开箱即用的 **OpenAI 风格** VLM 推理服务器,支持 W8A8 加速。
### 快速开始
1. **配置** `config/config.yaml`:
```yaml
model_name_or_path: /path/to/your/model # MLX VLM model (e.g., Qwen3-VL-2B W8A16)
sampling:
max_new_tokens: 1024
temperature: 1.0
top_p: 1.0
server:
host: 0.0.0.0
port: 8341
ttl: 1800
w8a8:
mode: 'off' # 'auto' | 'on' | 'off'
```
- `auto`:若硬件支持则启用 W8A8,否则回退到默认设置
- `on`:强制启用 W8A8(不支持时报错)。"选择 'on' 时,表示你的模型需要执行在线激活量化(online activation quantization)。在这种情况下,Cider 本身**不**保证量化精度,你需要自行应用某些量化算法,例如 SmoothQuant、QuaRot、GPTQ 或 QAT,以确保激活量化后精度不会明显下降。此选项只是让你在模型采用 W8A8 时能够利用硬件的计算优势,而不仅仅是模拟量化。"
- `off`:禁用 W8A8,使用标准 MLX 推理
2. **启动服务器**:
```bash
cd vlm_service
python server.py --config ../config/config.yaml
```
3. **发送请求**(OpenAI 风格 API):
```bash
# Text-only
curl http://localhost:8341/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "vlm",
"messages": [{"role": "user", "content": "Hello"}],
"stream": false
}'
# With image (base64)
curl http://localhost:8341/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "vlm",
"messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."} },
{"type": "text", "text": "What is in this image?"}
]}],
"stream": true
}'
```
### API 端点
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/chat/completions` | POST | 对话补全(流式 / 非流式) |
| `/v1/models` | GET | 列出可用模型 |
| `/health` | GET | 健康检查 |
| `/v1/queue` | GET | 请求队列状态 |
### 服务中 W8A8 的工作方式
当 `w8a8.mode` 为 `auto` 或 `on` 时,服务器在启动时会调用 `cider.convert_model()`,将所有 Linear 层替换为 `CiderLinear`。推理过程中:
- **Prefill**(处理输入 token,seq_len > 1):使用 W8A8 INT8 GEMM 以加速计算
- **Decode**(逐 token 生成,seq_len == 1):使用 INT8 MV kernel(接近原生速度)
无需修改代码——切换会根据输入序列长度自动进行。
## 架构
### MLX 自定义原语
W8A8Linear 与 W4A8Linear 均实现为 `mlx::core::Primitive` 的子类。这意味着:
1. **惰性求值(lazy evaluation)**:`y = layer(x)` 构建图节点,而非立即计算
2. **图组合**:多个原语调用组合为单个 MLX 图
3. **流调度**:MLX 的调度器处理 GPU 派发顺序
### Metal Kernel 流水线
每个原语都会派发 Metal 计算 kernel:
**Prefill(M > 1):**
1. **quantize_per_token**:FP16 激活 → INT8 + 逐 token 缩放
2. **matmul_fused_dequant**:INT8 × INT8 → INT32 → FP16(融合缩放反量化)
**Decode(M = 1):**
- **int8_mv**:直接 INT8 矩阵-向量乘积,权重反量化即时进行(无需激活量化)
对于 W4A8,GEMM 步骤在 fragment load 中包含内联 INT4→INT8 解包。
### TensorOps matmul2d
INT8 GEMM 使用 Apple 的 `mpp::tensor_ops::matmul2d(16, 32, 16)`——通过 Metal 4 的 `cooperative_tensor` API,在 M5+ 上可用的硬件加速 INT8×INT8→INT32 矩阵乘法。这与 MLX 的 NAX 模板可用的硬件指令相同。Cider 的 kernel 在存储阶段加入融合反量化(INT32 × scales → FP16),避免额外的设备内存往返。详见 [kernel comparison](benchmarks/mlx_native/cider_vs_mlx_int8.md)。
### Tile 配置
| Config | BM | BN | BK | SK | Threads | Use When |
|--------|----|----|----|----|---------|----------|
| Large | 128 | 128 | 512 | 32 | 512 | M > 64 |
| Small | 32 | 128 | 512 | 32 | 128 | M ≤ 64 |
根据 M 自动选择。包含 L2 cache swizzle 派发。
## ANE+GPU 异构张量并行(实验性)
我们发现,在 Mac 上进行推理时,仅利用了 GPU 与 CPU 两个硬件计算单元,而 Mac 上的 ANE(Apple Neural Engine)计算单元处于空闲状态。我们将此视为潜在的优化机会。受 [maderix/ANE](https://github.com/maderix/ANE), 启发,我们开展了 ANE+GPU 混合推理模式的实验工作。目前我们将该方案应用于张量并行计算。在 M4 芯片上,在仅同步的前向推理期间(MLX 原生使用称为惰性求值的技术以降低同步开销;在端到端测试中,混合推理目前尚未显示出优势,主要因为我们尚未基于 MLX 的惰性求值实现该方案——这仍是后续工作),与纯 GPU 推理在同步流水线(synchronize pipeline)下相比,我们观察到约 **3%~16%** 的性能提升。这仍属探索性工作,端到端收益目前受限于缺少与惰性求值兼容的实现。
在 LLM prefill 阶段,GPU 的矩阵单元已满载——但 **Apple Neural Engine 完全空闲**。ANE Split 利用这一点,沿输出通道拆分每个线性层的 GEMM:
- **ANE** 计算约 65% 的输出通道(FP32,通过逆向工程的私有 `_ANEClient` API)
- **GPU** 计算剩余约 35%(FP16,标准 MLX matmul)
- 两者**并发**运行,结果拼接
这是一种**异构张量并行**——不是数据并行,也不是流水线并行——利用同一 SoC 上的两个不同计算单元。
### 性能(Apple M4,Qwen3-VL-2B Prefill)
| seq | W8A16 GPU | SplitLinear | Speedup vs W8A16 |
|-----|----------|-----------|-------------|
| 512 | 639.9 ms | **615.9 ms** | **1.039×** |
| 1024 | 1348.6 ms | **1156.9 ms** | **1.17×** |
在测试的基准案例中,余弦相似度接近 1.0,top-1 token 一致率为 100%。
### 关键设计选择
- **仅 Prefill**:Decode 回退到原始 GPU linear(零开销)
- **共享输入准备**:Q/K/V 与 Gate/Up 投影通过 `_InputGroup` 共享单次输入转置+numpy 拷贝
- **自动路由**:Down 投影(IC > 2×OC)在 ANE 效率低下时保持仅 GPU
- **短序列绕过**:序列 < 192 token 时跳过分拆(开销大于收益)
完整文档、用法、构建说明与限制见 [`experimental/README.md`](experimental/README.md)。
> **注意:** ANE Split 已在 M4 上测试。M5 引入了 ANE 架构变更,可能破坏私有 API 桥接——尚未在 M5 上验证。
## 量化
| Component | Scheme | Granularity |
|-----------|--------|-------------|
| W8A8 weights | Symmetric INT8 | Per-channel or per-group (gs=64/128) |
| W4A8 weights | Symmetric INT4 (zp=8) | Per-column |
| Activations | Symmetric INT8 | Per-token |
| Accumulation | INT32 | — |
| Output dequant | `C_fp16 = C_int32 * s_act * s_weight` | Per-element |
## 限制
- **M=1 单算子**:逐通道 MV kernel 在独立 decode 调用时慢于 MLX W4A16。逐组 MV kernel 在端到端基准中与 MLX W8A16 decode 速度相差在 5% 以内。
- **INT8 TensorOps 仅支持 Apple M5+**:M4 及以下可安装但 `is_available()` 返回 False。
- **W4A8 慢于 W8A8**:INT4→INT8 解包 ALU 开销(Metal 4 matmul2d 无原生 INT4 操作数)。
## 工具
### 统一 PPL 评估
```bash
# Run all 5 configurations in one script
python tools/eval_ppl_all.py --num-samples 50
# Evaluates: FP16, W8A16 (MLX native), W8A8 per-channel, per-group(gs=64), per-group(gs=128)
# Outputs comparison table at the end
```
## 路线图
- [x] 一行式模型转换 API(`convert_model`,自动 prefill/decode)
- [x] 自动 dtype 处理(float16 / bfloat16)
- [x] 逐通道与逐组 W8A8 量化
- [x] 专用 decode MV kernel(匹配原生 MLX 速度)
- [x] 条件编译(M4 优雅回退)
- [x] mlx_vlm 与 mlx_lm 集成示例
- [x] 优化的 SDPA decode kernel(受 FlashInfer 启发,支持 GQA)
- [x] 一行式 `patch_sdpa()`,透明加速 decode
- [x] SDPA AutoTune(按架构块大小扫描)
- [ ] ANE 原语惰性求值
- [ ] 集成剪枝功能
- [ ] KVCache 量化
## 作者
Multimodal Team, Mininglamp Technology
如有 bug 报告、功能请求或使用问题,请在本仓库提交 issue。
## 引用
若您认为本工作有价值,请引用:
```bibtex
@software{wang2026cider,
author = {Multimodal Team, Mininglamp Technology},
title = {Cider: Exploiting Unused INT8 TensorOps for Faster LLM Prefill on Apple Silicon},
year = {2026},
howpublished = {https://github.com/Mininglamp-AI/cider}
}
```
## 许可证
MIT
## 致谢
- [MLX](https://github.com/ml-explore/mlx) by Apple — primitive API, NAXFrag kernel architecture
- [FlashInfer](https://github.com/flashinfer-ai/flashinfer) — inspired contiguous chunk GQA scheduling and register tiling for SDPA decode kernel
- Metal 4 MetalPerformancePrimitives for INT8 TensorOps
- [maderix/ANE](https://github.com/maderix/ANE) — inspired and informed our ANE+GPU tensor-parallel implementation