14MB 装下 45M 参数:拆解 Needle 的 .cact 单文件与 W4A8 量化魔法
【免费下载链接】needleAutomation foundation model for tiny devices: 2-bit, 8-29 MB, tool calls, ASR, structured extraction and embeddings on phones, wearables, smart homes, robots, cars and microcontrollers.项目地址: https://gitcode.com/GitHub_Trending/needle20/needle
当主流大模型以"GB 级"为单位搬运权重时,Cactus Compute 的 Needle 选择把 4500 万参数、分词器和量化码本全部塞进一个 14MB 的.cact二进制里,让手机、手表、树莓派和智能家居设备跑起本地工具调用 Agent。社区对它的讨论集中在三个数字上:14MB 单文件、W4A8 量化、28MB 运行内存。这篇文章不聊宣传口径,直接翻仓库源码,把.cact的字节布局、CQ 码本量化的数学、层优先张量布局和 KV 缓存预算逐层拆开,看看"小"到底是怎么设计出来的。
仓库的部署导出核心在 导出实现,量化数值在 量化模块,运行时内存预算在 架构定义。下面所有结论都以这几份源码为准。
一、45M 参数如何压进 14MB:CQ 码本量化
先看量化配置。在 量化模块 顶部写着三个硬编码:
WEIGHT_BITS = 4 ACT_BITS = 8 KV_BITS = 0这就是"W4A8"的出处:权重 4bit、激活 8bit。需要说明的是,这一套配置不是"拿个 scaler 一除了事",而是 Cactus Quant(CQ)——一种基于单位球码本 + Walsh-Hadamard 变换的结构化量化。
导出注释里写得很直白(见 导出实现 文件头):
Format is CQ-WxA8: matmul weights use the per-tensor CQ width selected by the deployment map; norms, Hadamard diagonals, and gates stay FP16.
翻译过来:参与矩阵乘的权重走 CQ 4bit,而 norm 的 scale、Hadamard 对角向量、门控系数这些"小张量"保持 FP16——它们量级小、数量少,不值得为省几个字节引入误差。
CQ 的具体编码分两步。第一步,把每个 group(默认 128 维)的权重行做归一化 Walsh-Hadamard 旋转,旋转后向量的方向用最近邻匹配到 Lloyd-Max 单位球码本,只存码本索引;向量的长度(L2 norm)单独以 FP16 保存。重建公式在 导出实现 中就是一行:
w_group = (codebook_bits[idx] * norm) @ H # H = normalized Walsh-Hadamard(group)码本是离线用 Lloyd-Max 算法在 40 万高斯样本上迭代 200 轮拟合出来的(见 量化模块 的_lloyd_max_gaussian),并且 2bit/3bit/4bit 三套码本(cb2[4] | cb3[8] | cb4[16])拼在一起放进文件头,运行时按位宽直接索引。索引本身再用 LSB-first 半字节打包:每 8 个索引 OR 进一个 64bit 字,低半字节是偶数列——这是 SIMD 解包最友好的一种布局。
之所以要用码本 + Hadamard 而不是朴素对称量化,是因为它对激活分布"宽"的矩阵更稳:旋转后能量集中到少数系数上,码本量化只丢方向、保能量,重建误差更小,而且让 4bit 权重在工具调用这类高确定性任务上接近无损。微调侧也在用同一套数值——cq_ste以直通估计(STE)把量化写进梯度,训练时看到的就是部署时的 W4 权重。
二、一个文件装下一切:.cact 的字节布局
14MB 的关键不只是"权重量化了",更是"一切都在一个文件里、且免解析加载"。看 导出实现 的字节布局注释,.cact只有三个区段:
1. 固定头部(48 个 u32 字段 + rope_theta 一个 f32)。它承载完整架构几何:tag、张量数、码本长度、KV 窗口与 KV 位宽、词表、d_model、头数、层数、head 维度、最大序列长、Hadamard 规模、mHC 车道数、滑窗宽度、全局注意力层掩码、QKV 因果卷积 taps、engram 槽位/阶数/站点、rope_theta……运行时只读头部就知道整个模型长什么样,同一套引擎可以加载任意配置的该架构。
2. 无名张量目录。每个记录{dtype, ndim, pad, shape[4], offset, nbytes, group_size, bits},没有任何名字。张量按固定规范顺序排列,运行时按位置索引:embedding → 逐层的 norm_in/q/k/v/gate/out 等 → final_norm → probe heads → 最后的 RAW 分词器。没有名字就省掉了哈希表和字符串比较,加载 = 顺序扫一遍 44 字节记录。
3. 64 字节对齐的张量 blob。每个 blob 从对齐偏移处开始,目录里已经存好了 offset,运行时按偏移直读。
三个区段叠加的效果是:整个文件可以被 mmap 或直接链进只读段,全程零反序列化。这正是仓库对部署权重的定义——"the deployment weights the C++ inference reads (mmap or linked read-only section; no parsing)"。
分词器也被"焊"进文件尾部,作为唯一的 RAW 张量:一个自包含的 SentencePiece BPE dump,含n_pieces、pad/eos/bos/unk id、add_dummy_prefix、byte_fallback,以及按 id 顺序排列的(f32 score, u8 type, u16 surface_len, UTF-8 bytes)记录(见 导出实现 的_tokenizer_blob和 分词器模块 的CHAT_MARKERS——工具调用的<tool_call>、<schema>等标记以 USER_DEFINED 类型内嵌)。社区里常被提到的一个"不可变"由此而来:SentencePiece 分词器硬编码在 .cact 归档里,导出时从 base archive 里原样取出(read_tokenizer_blob),无法替换——非英语场景的 token 数会显著膨胀,这正是"省体积"换来的部署约束。
三、层优先布局与预转置:为 GEMV/GEMM 定制的工作集
量化只能省体积,能不能快、省内存,取决于张量怎么摆。.cact用了两条硬规则,注释里写得很明确:
Weights are PRE-TRANSPOSED to [out, in] so each output row is contiguous along the reduction axis (what a GEMV/GEMM stream wants, and the axis quant groups run along).
预转置到 [out, in]:输出维的每一行沿规约轴(in)连续存放。解码阶段是典型的 GEMV——激活向量沿权重矩阵的 in 轴做点积,in 轴连续 = 顺序读取、完美契合缓存和 SIMD;预填充阶段是 GEMM,同样的布局让每一行就是一条规约流。量化 group 也沿同一轴切,解包出的码本索引 + norm 直接流式重建,无需转置中间态。
层优先(layer-major):embedding, then all of layer 0's tensors, ..., then final norm。也就是说,单层的全部张量(q/k/v/gate/out、norm scale、门控……)在文件里是物理连续的。推理 kernel 扫描一层时,工作集是文件里的一段连续区间,配合 mmap 的按页懒加载,理论上可以做到"走到哪层、缺页到哪层",未触碰的层不占物理内存。这正是小内存设备上"14MB 文件 + 28MB 运行内存"能成立的空间基础:权重不是被复制进堆里,而是被映射、被原位读取。
配合层优先的是--layers N的阶梯(ladder)导出:同一个 checkpoint 从 2 层到 20 层都是可部署模型(见 架构定义 的ladder_slice/ladder_config和 微调模块 的build_main)。2 层子网可以塞进比手机更小的设备,20 层完整模型留给树莓派级别的主机,文件格式完全不变。
四、28MB 运行内存:KV 缓存预算的"硬天花板"
W4 权重映射只读后,运行时最大的浮动变量就是KV 缓存。仓库没有让它失控,而是给了它一个明确的字节预算:
KV_BUDGET_BYTES = 11 * 1024 * 1024 + 512 * 1024 # ≈ 11.5MB(见 架构定义)。kv_budget_window按这个预算反推上下文窗口:对每层注意力,per_layer = kd + vd + (kd//KV_GROUP + vd//KV_GROUP) * 4字节(KV_GROUP=32),engram 站点另有per_site的固定开销;滑动窗口层和全局注意力层分开计算——默认配置里 20 层只有(4, 9, 14, 19)四层做全局注意力(global_layers),其余层走sliding_window=1024的局部注意力。窗口被max(KV_WINDOW_MIN, ...)和min(ctx, max_seq_len)双向钳制,保证 KV 缓存永远不超预算。
KV 本身也是 8bit(kv_bits=8,int8):查询按 head 做 A8 对称量化,KV 走逐 head 的 A8 fake-quant,和原生注意力 kernel 的数值完全对齐(见 量化模块 的a8_fake_quant_kv/maybe_quant_query)。而且kv_bits是写死在 blob 头里的——"a model quantized for one must be RUN at it",用错位宽的运行时 flag 会在数值上悄悄出错,所以它跟着权重走。
把账加一下:14MB 权重(mmap 只读、不拷贝)+ 约 11.5MB 的 KV 预算 + 激活与临时缓冲区,总运行内存落在 28MB 量级——这就是社区反复引用的"28MB 内存稳定运行"背后的工程机制:不是碰巧小,是每一项都被显式预算掉了。
五、从 checkpoint 到 .cact:微调与导出的闭环
仓库把"训练-导出-部署"做成了完整闭环(见 微调模块):
needle finetune data.jsonl --epochs 10 --out adapter.safetensors needle build --lora adapter.safetensors --layers 8 --out tuned.cactfinetune只训练 LoRA 适配器,目标锁定在注意力层的5 个投影矩阵(q_proj, k_proj, v_proj, gate_proj, out_proj,见LORA_TARGETS),基座冻结。有意思的是评分:_score_quantised用cq_ste_params(merged, WEIGHT_BITS)把 LoRA 合并后的权重再过一遍导出所用的 W4 量化再测精确调用率——"The float model is not what ships",微调好坏以部署数值为准。
build则做三件事:合并 LoRA、按--layers切阶梯、以 4bit 写.cact,并且分词器永远取自发布版 base archive(read_tokenizer_blob),微调者无法、也不需要重新训练它。这也带出了三个"不可变"边界,社区文章已反复确认,源码里同样可见:
- 置信度头不随本地微调训练:
needle build --lora直接丢弃置信度头,此类权重上报的confidence为None(见 运行时封装 与build_main); - 分词器硬编码于 .cact:无法替换;
- .cact 权重与 C++ 引擎版本强绑定:
_weight_generation用文件头 tag(0x05E12A83= v2、0x05E12A84= v3)选择兼容引擎,绝不让 v3 归档跑 v2 引擎。
六、一个引擎、三个 C 函数:端侧推理桥
最后落到"怎么跑"。Python 包不内置推理逻辑,它通过 ctypes 调一个**<1MB 的 C++ 引擎动态库**,API 极简(见 运行时封装 的_lib):
needle_load(data, len):把.cact权重整体交给引擎(一次加载,之后无法换权——"the engine cannot unload weights");needle_init(system, tools_json, tool_index_path):注册系统事实与工具 schema,返回 prefix token 数;needle_complete(text, ..., buffer, len):返回一个 JSON 信封,含function_calls、reasoning、confidence、prefill_tps/decode_tps,写入共享缓冲区。
lib.needle_complete.argtypes = [ ctypes.c_char_p, ctypes.POINTER(ctypes.c_float), ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]全程无对象模型、无序列化中间层:Python 传 UTF-8 字节,引擎把 token 约束到由 schema 编译的字节级文法上,保证输出必然可解析——工具调用要么是一个合法的 JSON 调用列表,要么是空列表(拒绝),没有第三种可能。加载微调权重时,工作进程 会把引擎放到独立子进程里,用长度前缀消息协议通信,把"一个进程一个模型"的约束变成可同时持有的隔离实例。
引擎的发布形态是每平台一个文件夹:macOS/Windows/Linux(x86_64、ARM64、ARMv7、RISC-V、MIPS)、Android、iOS、watchOS、tvOS、WASM 组件,全部共享同一套.cact容器(见 平台清单 的PLATFORMS与下面的部署图)。同一个 14MB 文件,映射进哪套引擎就在哪套引擎上跑——这也是 Whistle 语音模型敢"蹭"同一容器、同一引擎的原因。
结论:14MB 不是魔法,是一连串显式工程决策
回头再看标题里的"魔法":45M 参数压进 14MB,靠的是 CQ 码本量化 + FP16 小张量(W4A8);一个文件装下权重/分词器/码本,靠的是固定头 + 无名目录 + RAW 附件的自包含布局;28MB 运行内存,靠的是 mmap 只读映射 + 层优先物理布局 + 11.5MB KV 硬预算。每一环都在 导出实现、量化模块、架构定义 里有对应的源码证据,没有一项是宣传话术。
它同时给出了取舍的代价:分词器被焊死、置信度头不可本地训练、权重与引擎版本强绑定、非英语 token 膨胀。小,从来不是免费的——Needle 的价值在于把每一分体积都换成了可运行、可微调、可移植的确定性,而这正是端侧 Agent 真正需要的工程品质。
【免费下载链接】needleAutomation foundation model for tiny devices: 2-bit, 8-29 MB, tool calls, ASR, structured extraction and embeddings on phones, wearables, smart homes, robots, cars and microcontrollers.项目地址: https://gitcode.com/GitHub_Trending/needle20/needle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考