ParlzMAI: pure-C MoE inference + ParlzAIPlatformPAP framework

- pmai unified CLI (generate/chat/interactive/http/inspect/output/config)
- GPT+MoE transformer, .pap (f32/fp16/q8) + GGUF loader (order+version adaptive)
- llama/Mixtral arch: RoPE+GQA+SwiGLU+MoE (C==torch verified)
- C llama BPE tokenizer (validated vs llama-cpp-python)
- training framework + 0.1B/0.22B MoE models; quantization fp16/q8
- build artifacts to output/; HTTP API; config.yaml; scripts; openapi
这个提交包含在:
JGZYES
2026-09-07 07:16:58 +08:00
父节点 ff0b494778
当前提交 28535b1c74
修改 59 个文件,包含 71212 行新增0 行删除
+22
查看文件
@@ -0,0 +1,22 @@
# 构建/产物
build/
build-asan/
dist/
output/
# WSL 发行版镜像(大)
wsl/
# 模型文件(大,可重新训练/下载)
models/*.pap
models/*.gguf
!models/.gitkeep
# 缓存/二进制
*.o
*.a
*.so
*.core
*.papcache
__pycache__/
*.pyc
+136
查看文件
@@ -0,0 +1,136 @@
# moe-serve — 纯 C 的 MoE 推理服务
# ============================================
# Phase 1: 核心基础 (Tensor / Mat / Math / MemPool / Logger)
# Phase 2: GGUF 加载 + MoE 推理引擎
# Phase 3: HTTP/REST + WebSocket API (libmicrohttpd)
# Phase 4: 部署与监控
cmake_minimum_required(VERSION 3.15)
project(moe-serve LANGUAGES C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON) # gnu11: localtime_r 等 POSIX 函数
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# 构建产物统一放到 output/ 目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output)
# ---- 可选依赖探测 ----
# OpenMP: 用 -fopenmp 标志(编译+链接)交给活跃编译器解析 libgomp,
# 避免某些多 gcc 版本环境下解析成不存在的绝对库路径(如 gcc-11 的 libgomp.so)。
find_package(OpenMP QUIET)
set(MOE_OPENMP "")
set(MOE_HAS_OPENMP OFF)
if(OpenMP_C_FOUND)
set(MOE_OPENMP "-fopenmp")
set(MOE_HAS_OPENMP ON)
message(STATUS "OpenMP 已启用 (-fopenmp)")
else()
message(WARNING "未找到 OpenMP,将串行编译(可后续开启并行)")
endif()
function(moe_apply_openmp tgt)
if(MOE_HAS_OPENMP)
target_compile_options(${tgt} PRIVATE ${MOE_OPENMP})
target_link_options(${tgt} PRIVATE ${MOE_OPENMP})
endif()
endfunction()
# ---- HTTP 服务依赖 (libmicrohttpd / jansson),默认可选 ----
option(BUILD_HTTP "Build HTTP server support (--serve)" OFF)
set(MOE_HAVE_HTTP OFF)
if(BUILD_HTTP)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(MO_MHD IMPORTED_TARGET libmicrohttpd)
pkg_check_modules(MO_JANSSON IMPORTED_TARGET jansson)
if(MO_MHD_FOUND AND MO_JANSSON_FOUND)
set(MOE_HAVE_HTTP ON)
message(STATUS "HTTP --serve 可用 (libmicrohttpd ${MO_MHD_VERSION} + jansson)")
else()
message(WARNING "缺少 libmicrohttpd/jansson,--serve 不可用(apt-get install libmicrohttpd-dev libjansson-dev")
endif()
else()
message(WARNING "未找到 pkg-config,--serve 不可用")
endif()
endif()
# ---- 基础编译选项 ----
set(MOE_WARNINGS -Wall -Wextra -Wpedantic)
# ---- 库:core (tensor + matrix + transformer) ----
add_library(moe_core STATIC
src/core/tensor.c
src/core/matrix.c
src/core/transformer.c)
target_include_directories(moe_core PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_options(moe_core PRIVATE ${MOE_WARNINGS} -O3)
# ---- 库:utils (math + mempool + logger) ----
add_library(moe_utils STATIC
src/utils/math.c
src/utils/mempool.c
src/utils/logger.c)
target_include_directories(moe_utils PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_options(moe_utils PRIVATE ${MOE_WARNINGS} -O3)
# ---- 库:model (config + tokenizer + model/loader + gguf + llama) ----
add_library(moe_model STATIC
src/model/config.c
src/model/tokenizer.c
src/model/model.c
src/model/gguf.c
src/model/llama.c
src/model/llama_tokenizer.c)
target_include_directories(moe_model PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_options(moe_model PRIVATE ${MOE_WARNINGS} -O3)
# ---- 库:infer (sample + generate) ----
add_library(moe_infer STATIC
src/infer/sample.c
src/infer/generate.c)
target_include_directories(moe_infer PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_options(moe_infer PRIVATE ${MOE_WARNINGS} -O3)
set(MOE_INTERFACE_LIBS moe_infer moe_model moe_core moe_utils m)
# 静态库存在循环依赖 (moe_model↔moe_infer),用链接组解决
set(MOE_GROUP "-Wl,--start-group;moe_infer;moe_model;moe_core;moe_utils;-Wl,--end-group;m")
# ---- 可执行文件 ----
add_executable(pmai src/main.c)
target_include_directories(pmai PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(pmai PRIVATE ${MOE_WARNINGS} -O3)
target_link_libraries(pmai PRIVATE ${MOE_GROUP})
moe_apply_openmp(pmai)
if(MOE_HAVE_HTTP)
target_sources(pmai PRIVATE src/server/api.c)
target_link_libraries(pmai PRIVATE PkgConfig::MO_MHD PkgConfig::MO_JANSSON)
target_compile_definitions(pmai PRIVATE MOE_HTTP=1)
endif()
# ---- llama 架构运行器(构建到 output/----
add_executable(pmai-llama src/llama_main.c)
target_include_directories(pmai-llama PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(pmai-llama PRIVATE ${MOE_WARNINGS} -O3)
target_link_libraries(pmai-llama PRIVATE ${MOE_GROUP})
moe_apply_openmp(pmai-llama)
# ---- 测试 ----
enable_testing()
add_executable(test_core tests/test_core.c)
target_include_directories(test_core PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(test_core PRIVATE ${MOE_WARNINGS} -O2)
target_link_libraries(test_core PRIVATE ${MOE_GROUP})
moe_apply_openmp(test_core)
add_test(NAME core COMMAND test_core)
# ---- llama 分词器独立测试 ----
add_executable(test_llmtok tests/test_llmtok.c)
target_include_directories(test_llmtok PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_compile_options(test_llmtok PRIVATE ${MOE_WARNINGS} -O2)
target_link_libraries(test_llmtok PRIVATE ${MOE_GROUP})
moe_apply_openmp(test_llmtok)
+148
查看文件
@@ -0,0 +1,148 @@
# ParlzMAI · ParlzAIPlatformPAP
> **ParlzMAI**`pmai`):纯 C 的 **MoE 推理服务** + 自研 AI 框架 **ParlzAIPlatformPAP**。
> 自研模型格式 **`.pap`**、兼容 **GGUF(llama.cpp)**,在 WSL(自带 GPU) 上**真实训练出 0.1B MoE 模型**并端到端生成。
## 项目性质
- 纯 C 推理引擎(`src/`+ Python 训练框架(`platform/`+ 统一 CLI 入口 **`pmai`**。
- 支持两种模型:自研 GPT+MoE 架构(`.pap`)与 **llama/Mixtral 架构**GGUF)。
- 自定义 `.pap` 格式(`PAP1` 魔数 + 配置 + byte-level BPE 词表 + 命名张量,支持 f32/fp16/q8 量化)。
- 训练端可训 0.1B 级全层 MoE 模型,导出 `.pap` 或标准 `.gguf`
## 架构
```
┌──────────────────────────────────────────────┐
│ pmai (统一 CLI) │
│ 生成 · --interactive 对话 · --serve HTTP/SSE │
│ --inspect GGUF · --output 落盘 │
├──────────────────────────────────────────────┤
│ 推理引擎 (src/) C │
│ GPT: RMSNorm · Attention+KV · MoE(top-k) │
│ llama: RoPE · GQA · SwiGLU · Mixtral MoE │
│ 加载器: .pap(多dtype)/GGUF(多量化) │
│ 分词器: byte-level BPE / GPT-2 BPE │
│ 采样: top-k/temperature │
├──────────────────────────────────────────────┤
│ 训练框架 (platform/) Python │
│ GPT+MoE/pt · BPE · .pap/GGUF 读写 · 量化 │
├──────────────────────────────────────────────┤
│ WSL2(Ubuntu 22.04) + GPU RTX5060/CUDA │
└──────────────────────────────────────────────┘
```
## 目录结构
```
src/ ParlzMAI C 引擎
core/ tensor/matrix + transformer(GPT) + KVCache
model/ config + tokenizer + model(.pap) + gguf(容器/反量化) + llama(架构)
infer/ sample(top-k) + generate
server/ HTTP 服务 (libmicrohttpd + SSE)
utils/ math/mempool/logger
platform/ ParlzAIPlatformPAP (Python)
tokenizer.py pap_format.py gguf_format.py model.py llama_model.py llama_tokenizer.py
train.py export_gguf.py quant_pap.py verify.py verify_llama.py make_random.py
data/ 语料 (tinyshakespeare.txt) models/ .pap/.gguf 模型
output/ 构建产物 (pmai / pmai-llama / libmoe_*) scripts/ 脚本
```
## 构建与运行(WSL,Ubuntu-22.04
```bash
cd /mnt/f/ParlzMAI
cmake -S . -B build && cmake --build build -j
ctest --test-dir build # 单元测试
# 构建产物统一在 output/ 目录:pmai(统一入口) / pmai-llama(llama 运行器)
./output/pmai --model models/moe-0.1b-fp16.pap --prompt "ROMEO:" --n_tokens 120 --temperature 0.8 --top_k 40
./output/pmai --model models/moe-0.1b-fp16.pap --interactive # 对话式
./output/pmai --model models/moe-0.1b-fp16.pap --serve --port 8080 # HTTP/SSE
./output/pmai --model models/moe-0.1b-fp16.pap --prompt "QUEEN:" --output output/out.txt
./output/pmai --inspect models/moe-0.1b.gguf # 查看 GGUF
```
## 训练(GPU
```bash
/opt/pap-venv/bin/python platform/train.py --data data/tinyshakespeare.txt \
--out models/moe-0.1b.pap --vocab 2048 --layers 6 --d_model 512 --heads 8 \
--experts 16 --top_k 2 --d_expert 1024 --max_seq 256 --steps 2500 --batch 8
# 约 109M 参数(0.1B),全层 MoE
```
## 量化 / 格式转换
| 操作 | 命令 |
|---|---|
| `.pap` → fp16 (≈2×) | `python platform/quant_pap.py --src a.pap --dst a-fp16.pap --dtype fp16` |
| `.pap` → q8 (≈3.8×) | `python platform/quant_pap.py --src a.pap --dst a-q8.pap --dtype q8` |
| `.pap``.gguf` | `python platform/export_gguf.py --src a.pap --dst a.gguf --dtype fp16` |
实测(自研 0.1B = 109,238,784 参数):
| 文件 | 体积 | 相对 f32 | 贪心输出 |
|---|---|---|---|
| `moe-0.1b.pap` (f32) | 437.0 MB | 1.0× | 基准 |
| `moe-0.1b-fp16.pap` | 218.5 MB | 2.0× | 一致 |
| `moe-0.1b-q8.pap` | 116.1 MB | 3.76× | 一致 |
| `moe-0.1b.gguf`(fp16)| 218.5 MB | 2.0× | 一致 |
## 一致性验证
```bash
# GPT-MoEC vs torch 贪心
/opt/pap-venv/bin/python platform/verify.py --pap models/_smoke.pap # [MATCH] YES
# llama 架构:C vs torch 贪心(合成 MoE GGUF round-trip
/opt/pap-venv/bin/python platform/verify_llama.py # [MATCH] YES
# llama BPE 分词器 vs llama-cpp-python(真实 Qwen2-MoE 词表)
/opt/pap-venv/bin/python -c "..." # 逐 token MATCH
```
---
# 项目状态
## ✅ 已实现(已验证)
### 环境与基建
- WSL2 专用镜像 **Ubuntu 22.04** + 工具链(gcc/cmake/make/pip)。
- **GPU 直通**RTX 5060 / 8GB,CUDA 可用;venv `/opt/pap-venv`torch 2.14+cu130 / numpy)。
- **llama-cpp-python**(参考/逐 token 对照)+ **HF 镜像**(可下载真实模型)。
### 推理引擎(C,`src/`
- ✅ tensor / matrix / math / mempool / logger;`-Wall -Wextra` 干净编译;ctest 全绿。
-**GPT-MoE 前向**RMSNorm、带 KV Cache 注意力、MoE(router+Top-K+专家加权)、GFU FFN。
-**llama/Mixtral 架构**RoPE(NeoX) + GQA + SwiGLU + Mixtral MoE(重归一化 Top-k) —— **C 与 PyTorch 贪心逐 token 一致(`[MATCH] YES`**
-**byte-level BPE**GPT/自研两端一致)与 **GPT-2 byte-level BPE 分词器**`platform/llama_tokenizer.py`,**与 llama-cpp-python 逐 token MATCH,含中文**)。
-**`.pap` 加载器**(多 dtype f32/fp16/q8 反量化);**GGUF 加载器**(容器解析、元数据查询、F32/F16/BF16/Q4_0/Q5_0/Q5_1/Q8_0 反量化)。
-**真实 GGUF 兼容**:**顺序探测**(元数据/张量在前)+ **版本自适应**v1 用 u32、v2+ 用 u64)。
- ✅ 采样(top-k/temperature+ 自回归生成。
### CLI`pmai`,构建到 `output/`
-`--prompt` 生成、**`--interactive` 对话**(输入一行→流式生成→exit 退出)、**`--serve` HTTP/SSE**`POST /api/generate``GET /api/models``GET /health`)、**`--inspect` 查看任意 GGUF**、**`--output <file>` 落盘**(默认存 `output/`,交互自动 `output/chat-<ts>.txt`)。
-`pmai-llama`llama 架构 GGUF 运行器(加载 + 生成 id)。
### 训练框架(Python,`platform/`
- ✅ BPE 训练/编码(O(n log n));GPT+MoE(PyTorch);`.pap`/`gguf` 读写;量化(fp16 ≈2×、q8 ≈3.76×);导出 `.pap`/`.gguf`
-**真实训练并验证**GPU 训练 **0.1B109MMoE**tiny_shakespeare,2500 步 ≈11 分钟,loss 7.79→3.73),产出 `.pap`(437MB)/fp16(218MB)/q8(116MB)/`.gguf`(218MB)。
## ⬜ 未实现 / 待办(如实)
| 类别 | 内容 |
|---|---|
| **qwen2moe 架构运行** | 真实 `Qwen2.5-MoE-2X1.5B`(Q4_K_M) 需:**K 系反量化**(Q4_K/Q5_K/Q6_K/Q8_K/Q2_K/Q3_K,已取到 llama.cpp 源码未移植) + **qwen2moe 分支**(共享专家 `ffn_*_shexp` / 3D 路由专家 `ffn_*_exps` / 双重 router / 注意力 bias)。**未发布未经验证实现**。 |
| **C 端 llama 分词器** | `llama_tokenizer.py` 已验证,但 C 端未接入 → `pmai`/`pmai-llama` 对 llama 系列暂输出 id、非文本。 |
| **llama 真实文本生成对拍** | 需上两条都完成后,用 `pmai-llama` 与 llama-cpp-python 逐 token 一致才算跑通。 |
| **推理增强** | 无 batch 推理、无 prefill 批量化、无 mmap 大模型加载、无专家预加载/热切换。 |
| **Phase 3 剩余** | HTTP/SSE/健康检查已做;缺 WebSocket、线程池/请求调度、并发限流。 |
| **Phase 4 运维** | 无 config.yaml、无 QPS/延迟监控、无优雅关闭/热重载、无模型下载工具、无 wrk/ab 基准、无 OpenAPI 文档。 |
| **精简** | GGUF v1 旧格式模型(如 `klosax` 的 v1 目录)未能完全解析(不影响现代 v2/v3)。 |
| **Tokenizer 特殊 token** | `n_special=0`,未内置 `<|im_start|>` 等。 |
| **交互上下文** | `--interactive` 为无状态(每轮仅用当前输入,不携带上文)。 |
| **训练质量** | 语料 1MB、2500 步,仅“莎士比亚腔”,要更连贯需更大语料 + 长训练。 |
## 下一步(按优先级)
1. **C 端 llama BPE 分词器**`pmai`/`pmai-llama` 对任何 llama/GPT-2 系 GGUF 输出真实文本(边界清晰、可逐 token 验证)。
2. **K 系反量化移植**Q4_K/Q5_K/Q6_K/Q8_K 等,从 llama.cpp 源码逐行核对)。
3. **qwen2moe 架构分支** + 与 llama-cpp-python 端到端对拍 → `pmai-llama` 真跑 `Qwen2.5-MoE-2X1.5B`
4. 批推理 / mmap / QPS 监控 / config.yaml / 模型下载。
## 环境备注
- 构建/训练跑在 WSL `Ubuntu-22.04``wsl -d Ubuntu-22.04`
- 项目 Windows `F:\ParlzMAI` ↔ WSL `/mnt/f/ParlzMAI`
- 预处理缓存 `data/tinyshakespeare.txt.papcache`(BPE,避免重跑 ~4.6 分钟)。
+11
查看文件
@@ -0,0 +1,11 @@
# pmai 示例配置 config.yaml--config 加载,命令行参数优先)
server:
host: 0.0.0.0
port: 11434
model: models/moe-0.1b-fp16.pap # 也支持 .gguf
inference:
n_tokens: 100
temperature: 0.8
top_k: 40
+21915
查看文件
文件差异内容过多而无法显示 加载差异
+3115
查看文件
文件差异内容过多而无法显示 加载差异
文件差异内容过多而无法显示 加载差异
+51
查看文件
@@ -0,0 +1,51 @@
openapi: 3.0.0
info:
title: ParlzMAI API
version: 1.0.0
description: pmai HTTP 服务(--serve,需 -DBUILD_HTTP=ON)。MoE 推理生成。
servers:
- url: http://127.0.0.1:11434
paths:
/health:
get:
summary: 健康检查
responses:
'200': { description: ok }
/api/models:
get:
summary: 已加载模型信息
responses:
'200':
description: JSON 模型信息
content:
application/json:
schema:
type: object
properties:
model: { type: string }
params: { type: integer }
arch: { type: string }
n_experts: { type: integer }
/api/generate:
post:
summary: 生成(SSE 流式)
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
prompt: { type: string, example: 'ROMEO:' }
options:
type: object
properties:
max_tokens: { type: integer, example: 60 }
temperature: { type: number, example: 0.8 }
top_k: { type: integer, example: 40 }
responses:
'200':
description: SSE text/event-stream,每事件 data: {"response":"..."}
content:
text/event-stream:
schema: { type: string }
+41
查看文件
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""把自研 MoE 模型的 .pap 导出成标准 GGUF 容器。
用法:
/opt/pap-venv/bin/python platform/export_gguf.py \
--src models/moe-0.1b.pap --dst models/moe-0.1b.gguf --dtype fp16 # 或 q8 / f32
"""
from __future__ import annotations
import argparse
import os
import pap_format
import gguf_format
GML = {"f32": 0, "fp16": 1, "q8": 8}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--src", required=True)
ap.add_argument("--dst", required=True)
ap.add_argument("--dtype", default="fp16", choices=GML.keys())
args = ap.parse_args()
cfg, vocab, merges, tensors = pap_format.read_pap(args.src)
gguf_format.save_gguf(args.dst, cfg, vocab, merges, tensors, GML[args.dtype])
in_sz = os.path.getsize(args.src)
out_sz = os.path.getsize(args.dst)
print(f"已导出: {args.src} ({in_sz/1e6:.1f} MB) -> {args.dst} ({out_sz/1e6:.1f} MB)")
# 读回验证
rt, meta, ver = gguf_format.read_gguf(args.dst)
print(f"读回校验: version={ver} tensors={len(rt)} metadata={len(meta)}")
print(f" general.architecture = {meta.get('general.architecture')}")
print(f" pap.vocab_size = {meta.get('pap.vocab_size')} pap.moe_n_experts = {meta.get('pap.moe_n_experts')}")
if __name__ == "__main__":
main()
+404
查看文件
@@ -0,0 +1,404 @@
# -*- coding: utf-8 -*-
"""
GGUF (llama.cpp) v2/v3 容器 读取/写入,用于把 ParlzAIPlatformPAP 的自研 MoE 模型
导出成标准 GGUF,并能在 ParlzMAI (C 引擎) 中加载运行。也支持读任意 GGUF 做查看/量化分析。
张量命名沿用 .pap 的名字。元数据约定:
pap.* (见 model.py / C 端 model_load_gguf)
"""
from __future__ import annotations
import struct
from typing import Dict, List, Tuple
import numpy as np
try:
import pap_format as pf
except Exception: # pragma: no cover
pf = None
_M = "<"
MAGIC = b"GGUF"
# ggml 类型
GML_F32 = 0
GML_F16 = 1
GML_Q4_0 = 2
GML_Q5_0 = 6
GML_Q5_1 = 7
GML_Q8_0 = 8
# gguf 元数据值类型
GV_U8 = 0
GV_U32 = 4
GV_F32 = 6
GV_STRING = 8
GV_ARRAY = 9
TYPE_NAME = {0: "F32", 1: "F16", 2: "Q4_0", 6: "Q5_0", 7: "Q5_1", 8: "Q8_0"}
def _u64(v):
return struct.pack(_M + "Q", int(v))
def _u32(v):
return struct.pack(_M + "I", int(v) & 0xFFFFFFFF)
def _f32(v):
return struct.pack(_M + "f", float(v))
def _gstr(s: str) -> bytes:
b = s.encode("utf-8")
return _u64(len(b)) + b
def _meta(vt: int, value) -> bytes:
if vt == GV_U8:
return bytes([int(value)])
if vt == GV_U32:
return _u32(value)
if vt == GV_F32:
return _f32(value)
if vt == GV_STRING:
return _gstr(value)
if vt == GV_ARRAY:
inner, arr = value
if inner == GV_U8:
bb = bytes(arr)
return _u32(GV_U8) + _u64(len(bb)) + bb
if inner == GV_U32:
return _u32(GV_U32) + _u64(len(arr)) + b"".join(_u32(x) for x in arr)
if inner == GV_STRING:
return _u32(GV_STRING) + _u64(len(arr)) + b"".join(_gstr(s) for s in arr)
raise ValueError("不支持的数组内类型")
raise ValueError(f"未知元数据类型 {vt}")
def _tensor_bytes(arr: np.ndarray, gml_type: int) -> bytes:
w = np.ascontiguousarray(arr, dtype=np.float32).reshape(-1)
n = w.size
if gml_type == GML_F32:
return w.tobytes()
if gml_type == GML_F16:
return w.astype(np.float16).astype("<f2").tobytes()
if gml_type == GML_Q8_0:
pad = (-n) % 32
wp = np.pad(w, (0, pad), "constant") if pad else w
nb = wp.size // 32
b = wp.reshape(nb, 32)
maxabs = np.maximum(np.abs(b).max(axis=1), 1e-12)
d = (maxabs / 127.0).astype(np.float16)
# 每块: [fp16 d][32 int8]
out = bytearray()
for i in range(nb):
di = d[i]
q = np.clip(np.round(b[i] / float(di) / 1.0), -127, 127).astype(np.int8)
out += np.float16(di).astype("<f2").tobytes()
out += q.tobytes()
return bytes(out)
raise ValueError(f"未知 ggml 类型 {gml_type}")
def save_gguf(path: str, cfg, vocab: List[bytes], merges: List[Tuple[int, int]],
tensors: Dict[str, np.ndarray], gml_type: int = GML_F16) -> None:
"""导出 .gguf。cfg 为 pap_format.ModelConfig。gml_type 选 F16/Q8_0/F32。"""
from pap_format import ModelConfig
rows, cols = (cfg.d_model, cfg.vocab_size) # 占位断言,未用
names = sorted(tensors.keys())
# 计算张量表与 data 偏移
tensor_meta = []
data_chunks = []
offset = 0
for name in names:
w = np.ascontiguousarray(tensors[name], dtype=np.float32)
if w.ndim == 1:
w = w.reshape(1, -1)
r, c = w.shape
encoded = _tensor_bytes(w, gml_type)
# ggml dims: [ne0=列, ne1=行]
tensor_meta.append((name, [c, r], gml_type, offset))
data_chunks.append(encoded)
offset += len(encoded)
# 元数据
meta_kv: List[Tuple[str, int, object]] = [
("general.architecture", GV_STRING, "moe-pap"),
("general.name", GV_STRING, "ParlzAIPlatformPAP MoE 0.1B"),
("pap.vocab_size", GV_U32, cfg.vocab_size),
("pap.n_layer", GV_U32, cfg.n_layer),
("pap.d_model", GV_U32, cfg.d_model),
("pap.n_head", GV_U32, cfg.n_head),
("pap.d_ff", GV_U32, cfg.d_ff),
("pap.moe_n_experts", GV_U32, cfg.moe_n_experts),
("pap.moe_top_k", GV_U32, cfg.moe_top_k),
("pap.d_expert", GV_U32, cfg.d_expert),
("pap.max_seq_len", GV_U32, cfg.max_seq_len),
("pap.num_merges", GV_U32, cfg.num_merges),
("pap.rmsnorm_eps", GV_F32, cfg.rmsnorm_eps),
("pap.moe_mask", GV_ARRAY, (GV_U8, cfg.moe_mask)),
("pap.vocab_len", GV_ARRAY, (GV_U32, [len(t) for t in vocab])),
("pap.vocab_blob", GV_ARRAY, (GV_U8, b"".join(vocab))),
("pap.merges_a", GV_ARRAY, (GV_U32, [a for a, _ in merges])),
("pap.merges_b", GV_ARRAY, (GV_U32, [b for _, b in merges])),
]
with open(path, "wb") as f:
f.write(MAGIC)
f.write(_u32(3)) # version
f.write(_u64(len(tensor_meta))) # tensor_count
f.write(_u64(len(meta_kv))) # metadata_kv_count
for (k, vt, val) in meta_kv:
f.write(_gstr(k))
f.write(_u32(vt))
f.write(_meta(vt, val))
for (name, dims, t, off) in tensor_meta:
f.write(_gstr(name))
f.write(_u32(len(dims)))
for d in dims:
f.write(_u64(d))
f.write(_u32(t))
f.write(_u64(off))
# data 段(顺序与 tensor 表一致)
for chunk in data_chunks:
f.write(chunk)
def read_gguf(path: str):
"""读取 GGUF,返回 (tensors: dict, metadata: dict)。用于验证/查看。"""
data = open(path, "rb").read()
off = 0
def u32(o):
return struct.unpack_from(_M + "I", data, o)[0], o + 4
def u64(o):
return struct.unpack_from(_M + "Q", data, o)[0], o + 8
def gstr(o):
n, o = u64(o)
return data[o:o + n].decode("utf-8"), o + n
assert data[0:4] == MAGIC
version, off = u32(4)
tensor_count, off = u64(8)
meta_count, off = u64(16)
# 探测顺序:张量名总以 .weight 结尾
n0, po = u64(off)
tensor_first = data[po:po + n0].endswith(b".weight")
def _meta_loop(off):
metadata = {}
for _ in range(meta_count):
k, off = gstr(off)
vt, off = u32(off)
v, off = _read_val(data, off, vt)
metadata[k] = v
return metadata, off
def _tensor_loop(off):
tm = []
for _ in range(tensor_count):
name, off = gstr(off)
ndims, off = u32(off)
dims = []
for _ in range(ndims):
d, off = u64(off)
dims.append(d)
t, off = u32(off)
o, off = u64(off)
tm.append((name, dims, t, o))
return tm, off
if tensor_first:
tensor_meta, off = _tensor_loop(off)
metadata, off = _meta_loop(off)
else:
metadata, off = _meta_loop(off)
tensor_meta, off = _tensor_loop(off)
data_off = off
tensors = {}
for (name, dims, t, o) in tensor_meta:
n = 1
for d in dims:
n *= d
arr = _dequant(data, data_off + o, n, t)
if len(dims) >= 2:
arr = arr.reshape(dims[1], dims[0]) # ggml 行主序: (ne1 rows, ne0 cols) = (in,out)
tensors[name] = arr
return tensors, metadata, version
def read_gguf_meta(path: str):
"""只解析 header + 元数据 + 张量信息,不做反量化(用于读分词器/结构)。"""
data = open(path, "rb").read()
off = 0
def u32(o):
return struct.unpack_from(_M + "I", data, o)[0], o + 4
def u64(o):
return struct.unpack_from(_M + "Q", data, o)[0], o + 8
def gstr(o):
n, o = u64(o)
return data[o:o + n].decode("utf-8", errors="replace"), o + n
assert data[0:4] == MAGIC
version, off = u32(4)
tensor_count, off = u64(8)
meta_count, off = u64(16)
n0, po = u64(off)
tensor_first = data[po:po + n0].endswith(b".weight")
def _meta(off):
md = {}
for _ in range(meta_count):
k, off = gstr(off)
vt, off = u32(off)
v, off = _read_val(data, off, vt)
md[k] = v
return md, off
def _tens(off):
tm = []
for _ in range(tensor_count):
name, off = gstr(off)
nd, off = u32(off)
dims = []
for _ in range(nd):
d, off = u64(off)
dims.append(d)
t, off = u32(off)
o, off = u64(off)
tm.append((name, dims, t, o))
return tm, off
if tensor_first:
tmeta, off = _tens(off)
md, off = _meta(off)
else:
md, off = _meta(off)
tmeta, off = _tens(off)
return md, tmeta, version
def _read_val(data, off, vt):
def _u32(o):
return struct.unpack_from(_M + "I", data, o)[0], o + 4
if vt == GV_U8:
return data[off], off + 1
if vt == 1: # INT8
return struct.unpack_from(_M + "b", data, off)[0], off + 1
if vt == 2: # UINT16
return struct.unpack_from(_M + "H", data, off)[0], off + 2
if vt == 3: # INT16
return struct.unpack_from(_M + "h", data, off)[0], off + 2
if vt == GV_U32:
return struct.unpack_from(_M + "I", data, off)[0], off + 4
if vt == 5: # INT32
return struct.unpack_from(_M + "i", data, off)[0], off + 4
if vt == GV_F32:
return struct.unpack_from(_M + "f", data, off)[0], off + 4
if vt == 7: # BOOL
return bool(data[off]), off + 1
if vt == GV_STRING:
n, o = struct.unpack_from(_M + "Q", data, off)[0], off + 8
return data[o:o + n].decode("utf-8", errors="replace"), o + n
if vt == GV_ARRAY:
inner, o = _u32(off)
n, o = struct.unpack_from(_M + "Q", data, o)[0], o + 8
items = []
for _ in range(n):
it, o = _read_val(data, o, inner)
items.append(it)
return items, o
if vt == 10 or vt == 11: # UINT64 / INT64
return struct.unpack_from(_M + "q", data, off)[0], off + 8
if vt == 12: # FLOAT64
return struct.unpack_from(_M + "d", data, off)[0], off + 8
raise ValueError(f"未知元数据类型 {vt}")
def _dequant(data, off, n, gml_type):
if gml_type == GML_F32:
return np.frombuffer(data, np.float32, count=n, offset=off).astype(np.float32)
if gml_type == GML_F16:
return np.frombuffer(data, np.float16, count=n, offset=off).astype(np.float32)
if gml_type == GML_Q8_0:
nb = (n + 31) // 32
vals = np.empty(n, dtype=np.float32)
po = off
k = 0
for _ in range(nb):
scale = np.frombuffer(data, np.float16, count=1, offset=po).astype(np.float32)[0]
po += 2
cnt = min(32, n - k)
q = np.frombuffer(data, np.int8, count=cnt, offset=po)
po += cnt
vals[k:k + cnt] = q.astype(np.float32) * scale
k += cnt
return vals
raise ValueError(f"不支持读取 ggml 类型 {gml_type}")
def save_llama_gguf(path: str, cfg, tensors: Dict[str, "object"], gml_type: int = GML_F16,
tokens=None, merges=None, bos: int = 1, eos: int = 2,
name: str = "llama-moe"):
"""写 llama/Mixtral 架构的 GGUF。cfg 为 platform.llama_model.LlamaConfig。"""
import numpy as np
names = sorted(tensors.keys())
tensor_meta = []
data_chunks = []
offset = 0
for nm in names:
w = np.ascontiguousarray(tensors[nm], dtype=np.float32)
if w.ndim == 1:
w = w.reshape(1, -1)
r, c = w.shape
enc = _tensor_bytes(w, gml_type)
tensor_meta.append((nm, [c, r], gml_type, offset))
data_chunks.append(enc)
offset += len(enc)
meta_kv = [
("general.architecture", GV_STRING, "llama"),
("general.name", GV_STRING, name),
("llama.block_count", GV_U32, cfg.n_layer),
("llama.embedding_length", GV_U32, cfg.n_embd),
("llama.attention.head_count", GV_U32, cfg.n_head),
("llama.attention.head_count_kv", GV_U32, cfg.n_head_kv),
("llama.attention.layer_norm_rms_epsilon", GV_F32, cfg.rmsnorm_eps),
("llama.feed_forward_length", GV_U32, cfg.ffn_dim),
("llama.expert_count", GV_U32, cfg.n_expert),
("llama.expert_used_count", GV_U32, cfg.n_expert_used),
("llama.context_length", GV_U32, cfg.max_seq),
("llama.rope.dimension_count", GV_U32, cfg.head_dim),
("llama.rope.freq_base", GV_F32, cfg.rope_theta),
("llama.vocab_size", GV_U32, cfg.vocab_size),
("tokenizer.ggml.model", GV_STRING, "llama"),
("tokenizer.ggml.pre", GV_STRING, "default"),
]
if tokens is not None:
meta_kv.append(("tokenizer.ggml.tokens", GV_ARRAY, (GV_STRING, list(tokens))))
if merges is not None:
meta_kv.append(("tokenizer.ggml.merges", GV_ARRAY, (GV_STRING, list(merges))))
meta_kv.append(("tokenizer.ggml.bos_token_id", GV_U32, bos))
meta_kv.append(("tokenizer.ggml.eos_token_id", GV_U32, eos))
with open(path, "wb") as f:
f.write(MAGIC)
f.write(_u32(3))
f.write(_u64(len(tensor_meta)))
f.write(_u64(len(meta_kv)))
for (k, vt, val) in meta_kv:
f.write(_gstr(k))
f.write(_u32(vt))
f.write(_meta(vt, val))
for (nm, dims, t, off) in tensor_meta:
f.write(_gstr(nm))
f.write(_u32(len(dims)))
for d in dims:
f.write(_u64(d))
f.write(_u32(t))
f.write(_u64(off))
for chunk in data_chunks:
f.write(chunk)
+274
查看文件
@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""
ParlzAIPlatformPAP — llama/Mixtral 架构模型 (PyTorch)。
与 C 引擎 (src/core/llama.c) 数学一比一:
- pre-norm RMSNorm, 学习式绝对位置编码 -> 无;用 **RoPE** 旋转位置
- **GQA**: n_head 查询 / n_head_kv 键值
- **SwiGLU** FFN: down(silu(gate(x)) * up(x))
- **Mixtral MoE**: router softmax -> top-k(used) -> 重归一化 -> 专家加权和
激活: SiLU (x*sigmoid(x))。参数均以 (in, out) 行主序存储,C 端直接 x @ W。
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import List, Dict
import torch
import torch.nn as nn
import torch.nn.functional as F
@dataclass
class LlamaConfig:
vocab_size: int = 32000
n_layer: int = 4
n_embd: int = 512
n_head: int = 8
n_head_kv: int = 4
ffn_dim: int = 1024
n_expert: int = 8
n_expert_used: int = 2
max_seq: int = 256
rmsnorm_eps: float = 1e-5
rope_theta: float = 10000.0
@property
def head_dim(self) -> int:
return self.n_embd // self.n_head
def _rmsnorm(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
ms = x.pow(2).mean(dim=-1, keepdim=True)
return w * (x * torch.rsqrt(ms + eps))
def build_rope_cache(cfg: LlamaConfig, device=None):
"""cos/sin: [max_seq, head_dim/2] (NeoX half-rotate)"""
D = cfg.head_dim
half = D // 2
freqs = 1.0 / (cfg.rope_theta ** (torch.arange(0, half, dtype=torch.float32) / half))
t = torch.arange(cfg.max_seq, dtype=torch.float32)
angles = torch.outer(t, freqs) # [max_seq, half]
cos = torch.cos(angles)
sin = torch.sin(angles)
if device is not None:
cos, sin = cos.to(device), sin.to(device)
return cos, sin
def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, T: int) -> torch.Tensor:
"""x: [B, H, T, D]T 在 dim2"""
D = x.size(-1)
x1 = x[..., :D // 2]
x2 = x[..., D // 2:]
c = cos[:T].unsqueeze(0).unsqueeze(1) # [1,1,T,D/2]
s = sin[:T].unsqueeze(0).unsqueeze(1)
out = torch.cat([x1 * c - x2 * s, x2 * c + x1 * s], dim=-1)
return out
class LlamaBlock(nn.Module):
def __init__(self, cfg: LlamaConfig):
super().__init__()
self.cfg = cfg
H, HK, D = cfg.n_head, cfg.n_head_kv, cfg.head_dim
self.attn_norm = nn.Parameter(torch.ones(cfg.n_embd))
self.attn_q = nn.Parameter(torch.randn(cfg.n_embd, H * D) * 0.02) # (in,out)
self.attn_k = nn.Parameter(torch.randn(cfg.n_embd, HK * D) * 0.02)
self.attn_v = nn.Parameter(torch.randn(cfg.n_embd, HK * D) * 0.02)
self.attn_o = nn.Parameter(torch.randn(cfg.n_embd, cfg.n_embd) * 0.02)
self.ffn_norm = nn.Parameter(torch.ones(cfg.n_embd))
if cfg.n_expert > 0:
self.ffn_gate_inp = nn.Parameter(torch.randn(cfg.n_embd, cfg.n_expert) * 0.02) # router
self.ffn_experts = nn.ModuleList()
for _ in range(cfg.n_expert):
e = nn.Module()
e.ffn_gate = nn.Parameter(torch.randn(cfg.n_embd, cfg.ffn_dim) * 0.02)
e.ffn_up = nn.Parameter(torch.randn(cfg.n_embd, cfg.ffn_dim) * 0.02)
e.ffn_down = nn.Parameter(torch.randn(cfg.ffn_dim, cfg.n_embd) * 0.02)
self.ffn_experts.append(e)
else:
self.ffn_gate = nn.Parameter(torch.randn(cfg.n_embd, cfg.ffn_dim) * 0.02)
self.ffn_up = nn.Parameter(torch.randn(cfg.n_embd, cfg.ffn_dim) * 0.02)
self.ffn_down = nn.Parameter(torch.randn(cfg.ffn_dim, cfg.n_embd) * 0.02)
def forward(self, x, cos, sin):
cfg = self.cfg
B, T, C = x.shape
H, HK, D = cfg.n_head, cfg.n_head_kv, cfg.head_dim
hk = H // HK # 每组 kv 对应几个查询头
a = _rmsnorm(x, self.attn_norm, cfg.rmsnorm_eps)
q = a @ self.attn_q # [B,T,H*D]
k = a @ self.attn_k
v = a @ self.attn_v
q = q.view(B, T, H, D).transpose(1, 2) # [B,H,T,D]
k = k.view(B, T, HK, D).transpose(1, 2)
v = v.view(B, T, HK, D).transpose(1, 2)
q = _apply_rope(q, cos, sin, T)
k = _apply_rope(k, cos, sin, T)
weight = torch.zeros(B, H, T, T, device=x.device)
for h in range(H):
kvh = h // hk
qh = q[:, h] # [B,T,D]
kh = k[:, kvh] # [B,T,D]
sc = (qh @ kh.transpose(-2, -1)) / math.sqrt(D)
mask = torch.tril(torch.ones(T, T, device=x.device, dtype=torch.bool))
sc = sc.masked_fill(~mask, float("-inf"))
w = F.softmax(sc, dim=-1)
weight[:, h] = w
# 聚合
att = torch.zeros(B, T, H * D, device=x.device)
for h in range(H):
kvh = h // hk
vh = v[:, kvh] # [B,T,D]
o = weight[:, h] @ vh # [B,T,D]
att[:, :, h * D:(h + 1) * D] = o
att = att.view(B, T, C)
x = x + att @ self.attn_o
f = _rmsnorm(x, self.ffn_norm, cfg.rmsnorm_eps)
if cfg.n_expert > 0:
logits = f @ self.ffn_gate_inp # [B,T,n_expert]
probs = F.softmax(logits, dim=-1)
top = torch.topk(probs, k=cfg.n_expert_used, dim=-1)
wtop = top.values # [B,T,used]
idx = top.indices
wtop = wtop / (wtop.sum(dim=-1, keepdim=True) + 1e-9) # 重归一化
N = B * T
out = torch.zeros(N, C, device=x.device)
f2 = f.reshape(N, C)
for t in range(cfg.n_expert_used):
eidx = idx[..., t].reshape(-1)
w = wtop[..., t].reshape(-1, 1)
for e in range(cfg.n_expert):
sel = (eidx == e)
if sel.any():
exp = self.ffn_experts[e]
z = f2[sel] @ exp.ffn_gate
z = F.silu(z) * (f2[sel] @ exp.ffn_up)
out[sel] += w[sel] * (z @ exp.ffn_down)
x = x + out.view(B, T, C)
else:
g = F.silu(f @ self.ffn_gate)
u = f @ self.ffn_up
up = g * u
x = x + (up @ self.ffn_down)
return x
class LlamaMoE(nn.Module):
def __init__(self, cfg: LlamaConfig):
super().__init__()
self.cfg = cfg
self.token_embd = nn.Parameter(torch.randn(cfg.vocab_size, cfg.n_embd) * 0.02)
self.blocks = nn.ModuleList([LlamaBlock(cfg) for _ in range(cfg.n_layer)])
self.output_norm = nn.Parameter(torch.ones(cfg.n_embd))
self.output = nn.Parameter(torch.randn(cfg.n_embd, cfg.vocab_size) * 0.02)
self._cos = None
self._sin = None
def _rope(self):
if self._cos is None:
c, s = build_rope_cache(self.cfg)
self._cos, self._sin = c, s
return self._cos, self._sin
def forward(self, idx):
T = idx.size(1)
x = self.token_embd[idx] # [B,T,C]
cos, sin = self._rope()
for blk in self.blocks:
x = blk(x, cos, sin)
x = _rmsnorm(x, self.output_norm, self.cfg.rmsnorm_eps)
logits = x @ self.output
return logits
@torch.no_grad()
def generate_from_ids(self, init_ids, max_new, temperature=0.8, top_k=40):
device = next(self.parameters()).device
self.to(device)
x = torch.tensor([init_ids], dtype=torch.long, device=device)
out = list(init_ids)
for _ in range(max_new):
logits = self.forward(x[:, -self.cfg.max_seq:])[:, -1, :]
if temperature <= 0:
nxt = torch.argmax(logits).item()
else:
if temperature > 0: logits = logits / temperature
if top_k > 0:
v, _ = torch.topk(logits, top_k)
logits[logits < v[-1]] = float("-inf")
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1).item()
out.append(nxt)
x = torch.cat([x, torch.tensor([[nxt]], device=device)], dim=1)
return out
def export_tensors(model: LlamaMoE) -> Dict[str, torch.Tensor]:
"""收集为 GGUF llama 命名的 (in,out) 张量。"""
cfg = model.cfg
tens: Dict[str, torch.Tensor] = {}
tens["token_embd.weight"] = model.token_embd.detach()
for i, b in enumerate(model.blocks):
p = f"blk.{i}"
tens[f"{p}.attn_norm.weight"] = b.attn_norm.detach()
tens[f"{p}.attn_q.weight"] = b.attn_q.detach()
tens[f"{p}.attn_k.weight"] = b.attn_k.detach()
tens[f"{p}.attn_v.weight"] = b.attn_v.detach()
tens[f"{p}.attn_o.weight"] = b.attn_o.detach()
tens[f"{p}.ffn_norm.weight"] = b.ffn_norm.detach()
if cfg.n_expert > 0:
tens[f"{p}.ffn_gate_inp.weight"] = b.ffn_gate_inp.detach()
for e, ex in enumerate(b.ffn_experts):
tens[f"{p}.ffn_experts.{e}.ffn_gate.weight"] = ex.ffn_gate.detach()
tens[f"{p}.ffn_experts.{e}.ffn_up.weight"] = ex.ffn_up.detach()
tens[f"{p}.ffn_experts.{e}.ffn_down.weight"] = ex.ffn_down.detach()
else:
tens[f"{p}.ffn_gate.weight"] = b.ffn_gate.detach()
tens[f"{p}.ffn_up.weight"] = b.ffn_up.detach()
tens[f"{p}.ffn_down.weight"] = b.ffn_down.detach()
tens["output_norm.weight"] = model.output_norm.detach()
tens["output.weight"] = model.output.detach()
return tens
def load_llama_from_gguf(path: str, cfg: LlamaConfig) -> LlamaMoE:
"""从 GGUF 张量重建 LlamaMoE(用于 C/torch 对拍)。"""
import gguf_format
tens, meta, ver = gguf_format.read_gguf(path)
model = LlamaMoE(cfg)
sd: Dict[str, torch.Tensor] = {}
def T(k):
return torch.from_numpy(tens[k])
sd["token_embd"] = T("token_embd.weight")
sd["output_norm"] = T("output_norm.weight").reshape(-1)
sd["output"] = T("output.weight")
for i in range(cfg.n_layer):
b = f"blocks.{i}"
g = f"blk.{i}"
sd[f"{b}.attn_norm"] = T(f"{g}.attn_norm.weight").reshape(-1)
sd[f"{b}.attn_q"] = T(f"{g}.attn_q.weight")
sd[f"{b}.attn_k"] = T(f"{g}.attn_k.weight")
sd[f"{b}.attn_v"] = T(f"{g}.attn_v.weight")
sd[f"{b}.attn_o"] = T(f"{g}.attn_o.weight")
sd[f"{b}.ffn_norm"] = T(f"{g}.ffn_norm.weight").reshape(-1)
if cfg.n_expert > 0:
sd[f"{b}.ffn_gate_inp"] = T(f"{g}.ffn_gate_inp.weight")
for e in range(cfg.n_expert):
sd[f"{b}.ffn_experts.{e}.ffn_gate"] = T(f"{g}.ffn_experts.{e}.ffn_gate.weight")
sd[f"{b}.ffn_experts.{e}.ffn_up"] = T(f"{g}.ffn_experts.{e}.ffn_up.weight")
sd[f"{b}.ffn_experts.{e}.ffn_down"] = T(f"{g}.ffn_experts.{e}.ffn_down.weight")
else:
sd[f"{b}.ffn_gate"] = T(f"{g}.ffn_gate.weight")
sd[f"{b}.ffn_up"] = T(f"{g}.ffn_up.weight")
sd[f"{b}.ffn_down"] = T(f"{g}.ffn_down.weight")
model.load_state_dict(sd, strict=True)
model.eval()
return model
+73
查看文件
@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""GPT-2 / llama 的 byte-level BPE 分词器(读 GGUF tokenizer.ggml.tokens/merges)。"""
from __future__ import annotations
from typing import List, Tuple
def byte_to_unicode() -> dict:
"""GPT-2 的 byte→unicode 映射(与 llama.cpp’llama’ tokenizer 一致)。"""
bs = (list(range(ord("!"), ord("~") + 1)) +
list(range(ord("\u00a1"), ord("\u00ac") + 1)) +
list(range(ord("\u00ae"), ord("\u00ff") + 1)))
cs = bs[:]
n = 0
for b in range(256):
if b not in bs:
bs.append(b)
cs.append(256 + n)
n += 1
return dict(zip([b for b in bs], [chr(c) for c in cs]))
def unicode_to_byte() -> dict:
return {v: k for k, v in byte_to_unicode().items()}
class LlamaTokenizer:
def __init__(self, tokens: List[str], merges: List[str]):
self.tokens = tokens
# 字符 -> token id(只收录单字符 token
self.symbol2id = {t: i for i, t in enumerate(tokens) if len(t) == 1}
# merges: "a b" -> rank
self.ranks = {}
self.pairs = []
for i, m in enumerate(merges):
a, b = m.split(" ", 1) if " " in m else (m, "")
self.ranks[(a, b)] = i
self.pairs.append((a, b))
def encode(self, text: str, bos: bool = False, eos: bool = False,
bos_id: int = 151646, eos_id: int = 151643) -> List[int]:
b2u = byte_to_unicode()
syms = [b2u[b] for b in text.encode("utf-8")]
# BPE 合并(最小 rank
while len(syms) >= 2:
best = None
for i in range(len(syms) - 1):
r = self.ranks.get((syms[i], syms[i + 1]))
if r is not None and (best is None or r < best[0]):
best = (r, i)
if best is None:
break
_, i = best
syms[i] = syms[i] + syms[i + 1]
del syms[i + 1]
idmap = {t: i for i, t in enumerate(self.tokens)}
out = [idmap.get(s, self.symbol2id.get(s, 0)) for s in syms]
if bos:
out = [bos_id] + out
if eos:
out = out + [eos_id]
return out
def decode(self, ids: List[int]) -> str:
u2b = unicode_to_byte()
chars = []
for i in ids:
if 0 <= i < len(self.tokens):
chars.append(self.tokens[i])
s = "".join(chars)
out = bytearray()
for ch in s:
out.append(u2b.get(ch, ord(ch) & 0xFF))
return out.decode("utf-8", errors="replace")
+79
查看文件
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""生成一份多轮闲聊对话语料(User:/Assistant: 格式),用于训练聊天模型。"""
import random
def build_pools():
P = {}
P["names"] = ["Alice", "Bob", "Sam", "Luna", "Max", "Emma", "Kai", "Nora", "Leo", "Mia"]
P["hobbies"] = ["reading", "hiking", "cooking", "painting", "chess", "yoga", "photography", "fishing", "gaming", "gardening"]
P["foods"] = ["pizza", "sushi", "ramen", "tacos", "pasta", "salad", "biryani", "dumplings", "curry", "pancakes"]
P["cities"] = ["Tokyo", "Paris", "Berlin", "Lisbon", "Seoul", "Toronto", "Sydney", "Rome", "Kyoto", "Prague"]
P["weather"] = ["sunny", "rainy", "cloudy", "snowy", "windy", "clear"]
P["movies"] = ["sci-fi", "comedy", "drama", "horror", "adventure", "romance"]
P["music"] = ["pop", "jazz", "rock", "classical", "hip-hop", "folk"]
P["ports"] = ["pets", "cats", "dogs", "birds", "fish"]
return P
def gen_conversation(P, rng):
n_turns = rng.randint(3, 6)
turns = []
for i in range(n_turns):
if i % 2 == 0: # user
r = rng.random()
if r < 0.20:
u = rng.choice(["Hi", "Hello", "Hey", "Good morning", "What's up", "Hi there"])
elif r < 0.35:
u = f"How are you doing" + ("?" if rng.random() < 0.7 else "")
elif r < 0.5:
u = f"I like {rng.choice(P['hobbies'])}. What about you?"
elif r < 0.62:
u = f"Have you been to {rng.choice(P['cities'])}?"
elif r < 0.72:
u = f"What kind of {rng.choice(['food', 'music', 'movies'])} do you like?"
elif r < 0.8:
u = f"Do you have any {rng.choice(P['ports'])}?"
elif r < 0.9:
u = f"Can you tell me a {rng.choice(['joke', 'fact', 'story'])}?"
else:
u = "That's nice. Tell me more."
turns.append(("User", u))
else: # assistant
r = rng.random()
if r < 0.2:
a = rng.choice(["Hi! How can I help you today?", "Hello! It's good to hear from you.", "Hey there! What's on your mind?"])
elif r < 0.35:
a = rng.choice(["I'm doing great, thanks for asking!", "Pretty good! How about you?", "Can't complain. How are you?"])
elif r < 0.5:
a = f"That sounds fun! I enjoy {rng.choice(P['hobbies'])} too."
elif r < 0.62:
a = f"I haven't been to {rng.choice(P['cities'])}, but I've heard it's lovely."
elif r < 0.72:
a = f"I'd say I like {rng.choice(P['music'])} and {rng.choice(P['movies'])}. You?"
elif r < 0.8:
a = rng.choice(["I do! I really like dogs and cats.", "I think pets are wonderful companions."])
elif r < 0.9:
a = rng.choice(["Here's one: why did the programmer quit? Because he didn't get arrays.", "Did you know honey never spoils?", "Let me tell you a short story about a curious little robot."])
else:
a = rng.choice(["That makes sense.", "I see what you mean.", "Interesting point!", "Thanks for sharing that."])
turns.append(("Assistant", a))
return turns
def main(out="data/chat.txt", n=4000, seed=7):
random.seed(seed)
P = build_pools()
lines = []
for _ in range(n):
for role, text in gen_conversation(P, random):
lines.append(f"{role}: {text}")
lines.append("") # 空行分隔对话
with open(out, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"chat corpus -> {out} : {sum(len(l) for l in lines)} chars, {n} conversations")
if __name__ == "__main__":
import sys
main(out=sys.argv[1] if len(sys.argv) > 1 else "data/chat.txt")
+40
查看文件
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""生成一个随机小模型并导出 .pap,用于 C 与 torch 的对拍验证(不训练)。"""
import os
import model as model_mod
import pap_format
import tokenizer as tok
def main(out: str = "/mnt/f/ParlzMAI/models/_smoke.pap"):
# 纯字节词表(num_merges=0),最干净
vocab = [bytes([b]) for b in range(256)]
merges = []
cfg = {"vocab_size": 256, "n_layer": 2, "d_model": 64, "n_head": 4,
"d_ff": 128, "moe_n_experts": 4, "moe_top_k": 2, "d_expert": 128,
"max_seq_len": 32, "rmsnorm_eps": 1e-5, "moe_mask": [1, 0]}
model = model_mod.ParlzGPTMoE(cfg)
tens = model_mod.export_tensors(model)
pcfg = pap_format.ModelConfig()
pcfg.vocab_size = cfg["vocab_size"]
pcfg.n_layer = cfg["n_layer"]
pcfg.d_model = cfg["d_model"]
pcfg.n_head = cfg["n_head"]
pcfg.d_ff = cfg["d_ff"]
pcfg.moe_n_experts = cfg["moe_n_experts"]
pcfg.moe_top_k = cfg["moe_top_k"]
pcfg.d_expert = cfg["d_expert"]
pcfg.max_seq_len = cfg["max_seq_len"]
pcfg.num_merges = len(merges)
pcfg.tie_weights = 0
pcfg.rmsnorm_eps = cfg["rmsnorm_eps"]
pcfg.moe_mask = list(cfg["moe_mask"])
os.makedirs(os.path.dirname(out), exist_ok=True)
pap_format.save_pap(out, pcfg, vocab, merges, tens)
print(f"exported random model -> {out}")
if __name__ == "__main__":
main()
+261
查看文件
@@ -0,0 +1,261 @@
# -*- coding: utf-8 -*-
"""
ParlzAIPlatformPAP — GPT + MoE 模型 (PyTorch)。
与 C 引擎 (src/core/transformer.c) 数学一对一:
- pre-norm transformer, RMSNorm
- 学习式绝对位置编码 (无 RoPE)
- 缩放点积注意力 (无 GQA), 因果掩码
- 每个 block: norm1 -> attn -> +res, norm2 -> ffn/moe -> +res
- FFN 层可选 MoE: router 线性 + softmax -> top-k -> 专家加权和
- 激活: tanh 近似 GELU (与 C 的 mo_gelu 一致)
导出 .pap 时所有 nn.Linear 权重做 .t() 转置成 (in,out),C 端直接 x @ W。
"""
from __future__ import annotations
import math
from typing import Dict, List
import torch
import torch.nn as nn
import torch.nn.functional as F
def rmsnorm(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
"""y = x / sqrt(mean(x^2)+eps) * w,沿最后一维。"""
mean_sq = x.pow(2).mean(dim=-1, keepdim=True)
x = x * torch.rsqrt(mean_sq + eps)
return w * x
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x):
return rmsnorm(x, self.weight, self.eps)
class CausalSelfAttention(nn.Module):
def __init__(self, d_model: int, n_head: int):
super().__init__()
assert d_model % n_head == 0
self.n_head = n_head
self.head_dim = d_model // n_head
self.d_model = d_model
self.wq = nn.Linear(d_model, d_model, bias=False)
self.wk = nn.Linear(d_model, d_model, bias=False)
self.wv = nn.Linear(d_model, d_model, bias=False)
self.wo = nn.Linear(d_model, d_model, bias=False)
def forward(self, x):
B, T, C = x.shape
q = self.wq(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = self.wk(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
v = self.wv(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
mask = torch.tril(torch.ones(T, T, device=x.device, dtype=torch.bool))
att = att.masked_fill(~mask, float("-inf"))
att = F.softmax(att, dim=-1)
y = att @ v
y = y.transpose(1, 2).contiguous().view(B, T, C)
return self.wo(y)
class MoEExpert(nn.Module):
def __init__(self, d_model: int, d_expert: int):
super().__init__()
self.w1 = nn.Linear(d_model, d_expert, bias=False) # 上投影
self.w2 = nn.Linear(d_expert, d_model, bias=False) # 下投影
def forward(self, x):
return self.w2(F.gelu(self.w1(x), approximate="tanh"))
class MoELayer(nn.Module):
def __init__(self, d_model: int, n_experts: int, top_k: int, d_expert: int):
super().__init__()
self.n_experts = n_experts
self.top_k = top_k
self.router = nn.Linear(d_model, n_experts, bias=False)
self.experts = nn.ModuleList([MoEExpert(d_model, d_expert) for _ in range(n_experts)])
def forward(self, x):
B, T, C = x.shape
logits = self.router(x) # (B,T,n_experts)
probs = F.softmax(logits, dim=-1) # 路由概率
topk = torch.topk(probs, k=self.top_k, dim=-1) # values, indices
sel_w = topk.values # (B,T,top_k)
sel_i = topk.indices # (B,T,top_k)
out = torch.zeros_like(x)
for t in range(self.top_k):
e_idx = sel_i[..., t].reshape(-1) # (B*T,)
wt = sel_w[..., t].reshape(-1, 1) # (B*T,1)
src = x.reshape(B * T, C)
# 用 gather 取每行对应专家的输出
exp_out = torch.zeros(B * T, C, device=x.device)
for e in range(self.n_experts):
sel = (e_idx == e)
if sel.any():
sub = self.experts[e](src[sel])
exp_out[sel] = sub
out += (wt * exp_out).view(B, T, C)
return out
class Block(nn.Module):
def __init__(self, d_model: int, n_head: int, cfg, is_moe: bool):
super().__init__()
self.is_moe = is_moe
self.ln1 = RMSNorm(d_model, cfg["rmsnorm_eps"])
self.attn = CausalSelfAttention(d_model, n_head)
self.ln2 = RMSNorm(d_model, cfg["rmsnorm_eps"])
if is_moe:
self.moe = MoELayer(d_model, cfg["moe_n_experts"], cfg["moe_top_k"], cfg["d_expert"])
else:
self.ffn = nn.Sequential(
nn.Linear(d_model, cfg["d_ff"], bias=False),
nn.GELU(approximate="tanh"),
nn.Linear(cfg["d_ff"], d_model, bias=False),
)
def forward(self, x):
x = x + self.attn(self.ln1(x))
if self.is_moe:
x = x + self.moe(self.ln2(x))
else:
x = x + self.ffn(self.ln2(x))
return x
class ParlzGPTMoE(nn.Module):
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.vocab_size = cfg["vocab_size"]
self.max_seq_len = cfg["max_seq_len"]
self.wte = nn.Embedding(cfg["vocab_size"], cfg["d_model"])
self.wpe = nn.Embedding(cfg["max_seq_len"], cfg["d_model"])
self.drop = nn.Dropout(0.0)
self.blocks = nn.ModuleList([
Block(cfg["d_model"], cfg["n_head"], cfg, cfg["moe_mask"][l])
for l in range(cfg["n_layer"])
])
self.ln_f = RMSNorm(cfg["d_model"], cfg["rmsnorm_eps"])
self.lm_head = nn.Linear(cfg["d_model"], cfg["vocab_size"], bias=False)
def forward(self, idx, targets=None):
T = idx.size(1)
assert T <= self.max_seq_len
pos = torch.arange(0, T, device=idx.device).unsqueeze(0)
x = self.wte(idx) + self.wpe(pos)
x = self.drop(x)
for blk in self.blocks:
x = blk(x)
x = self.ln_f(x)
logits = self.lm_head(x)
if targets is not None:
loss = F.cross_entropy(logits.view(-1, self.vocab_size), targets.view(-1))
return logits, loss
return logits, None
@torch.no_grad()
def generate_from_ids(self, init_ids, max_new, temperature=0.8, top_k=40):
"""自回归采样生成(训练途中示例用,无需 tokenizer)。返回 id 列表。"""
device = next(self.parameters()).device
x = torch.tensor([init_ids], dtype=torch.long, device=device)
out = list(init_ids)
for _ in range(max_new):
logits, _ = self(x[:, -self.max_seq_len:])
logits = logits[0, -1]
if temperature <= 0:
nxt = torch.argmax(logits).item()
out.append(nxt)
x = torch.cat([x, torch.tensor([[nxt]], device=device)], dim=1)
continue
if temperature > 0:
logits = logits / temperature
if top_k > 0:
v, _ = torch.topk(logits, top_k)
logits[logits < v[-1]] = float("-inf")
probs = F.softmax(logits, dim=-1)
nxt = torch.multinomial(probs, 1).item()
out.append(nxt)
x = torch.cat([x, torch.tensor([[nxt]], device=device)], dim=1)
return out
def export_tensors(model: "ParlzGPTMoE"):
"""收集权重为 .pap 命名张量;Linear 权重转置为 (in,out) 行主序。"""
tens: Dict[str, torch.Tensor] = {}
tens["wte"] = model.wte.weight.detach() # (vocab,d)
tens["wpe"] = model.wpe.weight.detach() # (max_seq,d)
for l, blk in enumerate(model.blocks):
p = f"l{l}"
tens[f"{p}.norm1"] = blk.ln1.weight.detach()
tens[f"{p}.attn.wq"] = blk.attn.wq.weight.detach().t()
tens[f"{p}.attn.wk"] = blk.attn.wk.weight.detach().t()
tens[f"{p}.attn.wv"] = blk.attn.wv.weight.detach().t()
tens[f"{p}.attn.wo"] = blk.attn.wo.weight.detach().t()
tens[f"{p}.norm2"] = blk.ln2.weight.detach()
if blk.is_moe:
tens[f"{p}.moe.router"] = blk.moe.router.weight.detach().t()
for e, expert in enumerate(blk.moe.experts):
tens[f"{p}.moe.exp{e}.w1"] = expert.w1.weight.detach().t()
tens[f"{p}.moe.exp{e}.w2"] = expert.w2.weight.detach().t()
else:
tens[f"{p}.ffn.w1"] = blk.ffn[0].weight.detach().t()
tens[f"{p}.ffn.w2"] = blk.ffn[2].weight.detach().t()
tens["norm_final"] = model.ln_f.weight.detach()
tens["lm_head"] = model.lm_head.weight.detach().t() # (vocab,d)->(d,vocab)? no
# 注意: lm_head 是 (vocab,d),C 端 h @ lm_head 需 (d,vocab)。torch Linear 权重是 (vocab,d),
# 转置应为 (d,vocab)。但 export 里我们要求 (in,out);lm_head 的 "输入"是 d,"输出"是 vocab。
tens["lm_head"] = model.lm_head.weight.detach().t() # 保证 (d_model, vocab_size)? 见下
return tens
def load_pap_model(path: str):
"""从 .pap 重建 torch 模型(反推 export_tensors 的转置)。返回 (model, vocab, merges)。"""
import pap_format
cfgp, vocab, merges, tens = pap_format.read_pap(path)
cfg = {
"vocab_size": cfgp.vocab_size,
"n_layer": cfgp.n_layer,
"d_model": cfgp.d_model,
"n_head": cfgp.n_head,
"d_ff": cfgp.d_ff,
"moe_n_experts": cfgp.moe_n_experts,
"moe_top_k": cfgp.moe_top_k,
"d_expert": cfgp.d_expert,
"max_seq_len": cfgp.max_seq_len,
"rmsnorm_eps": cfgp.rmsnorm_eps,
"moe_mask": list(cfgp.moe_mask),
}
model = ParlzGPTMoE(cfg)
sd = {}
sd["wte.weight"] = torch.from_numpy(tens["wte"])
sd["wpe.weight"] = torch.from_numpy(tens["wpe"])
for l in range(cfg["n_layer"]):
sd[f"blocks.{l}.ln1.weight"] = torch.from_numpy(tens[f"l{l}.norm1"]).reshape(-1)
sd[f"blocks.{l}.attn.wq.weight"] = torch.from_numpy(tens[f"l{l}.attn.wq"].T)
sd[f"blocks.{l}.attn.wk.weight"] = torch.from_numpy(tens[f"l{l}.attn.wk"].T)
sd[f"blocks.{l}.attn.wv.weight"] = torch.from_numpy(tens[f"l{l}.attn.wv"].T)
sd[f"blocks.{l}.attn.wo.weight"] = torch.from_numpy(tens[f"l{l}.attn.wo"].T)
sd[f"blocks.{l}.ln2.weight"] = torch.from_numpy(tens[f"l{l}.norm2"]).reshape(-1)
if cfg["moe_mask"][l]:
sd[f"blocks.{l}.moe.router.weight"] = torch.from_numpy(tens[f"l{l}.moe.router"].T)
for e in range(cfg["moe_n_experts"]):
sd[f"blocks.{l}.moe.experts.{e}.w1.weight"] = torch.from_numpy(tens[f"l{l}.moe.exp{e}.w1"].T)
sd[f"blocks.{l}.moe.experts.{e}.w2.weight"] = torch.from_numpy(tens[f"l{l}.moe.exp{e}.w2"].T)
else:
sd[f"blocks.{l}.ffn.0.weight"] = torch.from_numpy(tens[f"l{l}.ffn.w1"].T)
sd[f"blocks.{l}.ffn.2.weight"] = torch.from_numpy(tens[f"l{l}.ffn.w2"].T)
sd["ln_f.weight"] = torch.from_numpy(tens["norm_final"]).reshape(-1)
sd["lm_head.weight"] = torch.from_numpy(tens["lm_head"].T)
model.load_state_dict(sd, strict=True)
model.eval()
return model, vocab, merges
+293
查看文件
@@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
"""
ParlzAIPlatformPAP — .pap 模型文件格式 (version 1)
二进制布局 (little-endian):
off size 字段
0 4 magic = b"PAP1"
4 4 version (u32) = 1
8 4 dtype (u32) = 0 # 0=f32, 1=Q8(未实现)
12 4 vocab_size (u32)
16 4 n_layer (u32)
20 4 d_model (u32)
24 4 n_head (u32)
28 4 d_ff (u32) # 稠密 FFN 中间层
32 4 moe_n_experts (u32)
36 4 moe_top_k (u32)
40 4 d_expert (u32) # MoE 专家中间层
44 4 max_seq_len (u32)
48 4 num_merges (u32)
52 4 n_special (u32)
56 1 tie_weights (u8)
57 1 reserved
58 1 reserved
59 1 reserved
60 4 rmsnorm_eps (f32)
64 n moe_mask: n_layer bytes (1=MoE, 0=dense FFN)
--- vocab 段 ---
u32 vocab_blob_len
blob: for id in 0..vocab_size-1: u32 len + len bytes
for m in 0..num_merges-1 : u32 a + u32 b
--- weights 段 ---
u32 n_tensors
for each: u32 name_len + name + u32 rows + u32 cols + rows*cols f32(row-major)
张量命名(行主序,列为 (in, out),C 端直接 x @ W):
wte (vocab,d) wpe (max_seq,d)
l{L}.norm1, l{L}.attn.{wq,wk,wv,wo}, l{L}.norm2
l{L}.moe.router (d,n_experts) ; l{L}.moe.exp{E}.{w1(d,d_expert),w2(d_expert,d)}
l{L}.ffn.{w1(d,d_ff),w2(d_ff,d)} (稠密层)
norm_final (d) ; lm_head (vocab,d)
"""
from __future__ import annotations
import struct
from typing import Dict, List, Tuple
PARZ_MAGIC = b"PAP1"
VERSION = 1
_M = "<" # little-endian
def _u32(v: int) -> bytes:
return struct.pack(_M + "I", int(v) & 0xFFFFFFFF)
def _f32(v: float) -> bytes:
return struct.pack(_M + "f", float(v))
class ModelConfig:
def __init__(self):
self.vocab_size = 0
self.n_layer = 0
self.d_model = 0
self.n_head = 0
self.d_ff = 0
self.moe_n_experts = 0
self.moe_top_k = 0
self.d_expert = 0
self.max_seq_len = 0
self.num_merges = 0
self.n_special = 0
self.tie_weights = 0
self.rmsnorm_eps = 1e-5
self.moe_mask: List[int] = []
@property
def head_dim(self) -> int:
return self.d_model // self.n_head
def _tensor_to_f32(weight) -> "object":
"""任意 torch/numpy/list -> numpy float32 2D (in,out) 数组。"""
import numpy as np
if hasattr(weight, "detach"): # torch.Tensor
weight = weight.detach().cpu().numpy()
return np.asarray(weight, dtype=np.float32)
Q8_BLOCK = 32 # int8 量化块大小
def _tensor_bytes(w, dtype: int) -> bytes:
"""把一个 2D f32 数组编码成 dtype 字节流(行主序展平)。"""
import numpy as np
w = np.ascontiguousarray(w, dtype=np.float32).reshape(-1)
n = w.size
if dtype == 0: # f32
return w.tobytes()
if dtype == 1: # fp16
return w.astype(np.float16).astype("<f2").tobytes()
if dtype == 2: # q8: 每块 [fp16 scale][32×int8]
pad = (-n) % Q8_BLOCK
wp = np.pad(w, (0, pad), "constant") if pad else w
nb = wp.size // Q8_BLOCK
b = wp.reshape(nb, Q8_BLOCK)
scale = np.maximum(np.abs(b).max(axis=1), 1e-12)
q = np.clip(np.round(b / scale[:, None] * 127.0), -127, 127).astype(np.int8)
out = bytearray()
for i in range(nb):
out += np.float32(scale[i]).astype("<f2").tobytes() # 2 字节 fp16 scale
out += q[i].tobytes()
return bytes(out)
raise ValueError(f"未知 dtype {dtype}")
def _tensor_stored_bytes(rows, cols, dtype: int) -> int:
n = rows * cols
if dtype == 0:
return n * 4
if dtype == 1:
return n * 2
if dtype == 2:
nb = (n + Q8_BLOCK - 1) // Q8_BLOCK
return n + nb * 2
raise ValueError(f"未知 dtype {dtype}")
def save_pap(path: str, cfg: ModelConfig, vocab: List[bytes],
merges: List[Tuple[int, int]], tensors: Dict[str, "object"], dtype: int = 0) -> None:
"""写出 .pap 文件。tensors 的每个矩阵须为 (in, out) 行主序(C 端直接 x@W)。
dtype: 0=f32, 1=fp16, 2=int8(块量化)。"""
import numpy as np
with open(path, "wb") as f:
f.write(PARZ_MAGIC)
f.write(_u32(VERSION))
f.write(_u32(dtype)) # dtype
f.write(_u32(cfg.vocab_size))
f.write(_u32(cfg.n_layer))
f.write(_u32(cfg.d_model))
f.write(_u32(cfg.n_head))
f.write(_u32(cfg.d_ff))
f.write(_u32(cfg.moe_n_experts))
f.write(_u32(cfg.moe_top_k))
f.write(_u32(cfg.d_expert))
f.write(_u32(cfg.max_seq_len))
f.write(_u32(cfg.num_merges))
f.write(_u32(cfg.n_special))
f.write(bytes([cfg.tie_weights & 0xFF, 0, 0, 0]))
f.write(_f32(cfg.rmsnorm_eps))
# moe_mask
mask = bytes(cfg.moe_mask)
assert len(mask) == cfg.n_layer
f.write(mask)
# --- vocab blob ---
blob = bytearray()
for tok in vocab:
blob += _u32(len(tok))
blob += tok
for (a, b) in merges:
blob += _u32(a)
blob += _u32(b)
f.write(_u32(len(blob)))
f.write(bytes(blob))
# --- weights ---
names = sorted(tensors.keys())
f.write(_u32(len(names)))
for name in names:
w = _tensor_to_f32(tensors[name])
if w.ndim == 1:
w = w.reshape(1, -1) # 一维 norm 向量按单行保存
rows, cols = w.shape
nb = name.encode("utf-8")
f.write(_u32(len(nb)))
f.write(nb)
f.write(_u32(rows))
f.write(_u32(cols))
f.write(_tensor_bytes(w, dtype))
def read_pap(path: str):
"""读回 .pap,返回 (cfg, vocab, merges, tensors)。用于校验或转回 torch。"""
import numpy as np
tensors: Dict[str, np.ndarray] = {}
with open(path, "rb") as f:
data = f.read()
off = 0
off_r = lambda n: None
def r_u32(off):
return (struct.unpack_from(_M + "I", data, off)[0], off + 4)
r_u8 = None
magic = data[0:4]
assert magic == PARZ_MAGIC, f"bad magic {magic}"
cfg = ModelConfig()
def u32(off):
v, n = struct.unpack_from(_M + "I", data, off)[0], off + 4
return v, n
def f32(off):
v, n = struct.unpack_from(_M + "f", data, off)[0], off + 4
return v, n
v, off = u32(4)
assert v == VERSION
dtype, off = u32(8)
cfg.vocab_size, off = u32(12)
cfg.n_layer, off = u32(16)
cfg.d_model, off = u32(20)
cfg.n_head, off = u32(24)
cfg.d_ff, off = u32(28)
cfg.moe_n_experts, off = u32(32)
cfg.moe_top_k, off = u32(36)
cfg.d_expert, off = u32(40)
cfg.max_seq_len, off = u32(44)
cfg.num_merges, off = u32(48)
cfg.n_special, off = u32(52)
cfg.tie_weights = data[56]
off = 60
cfg.rmsnorm_eps, off = f32(60)
off = 64
cfg.moe_mask = list(data[off:off + cfg.n_layer])
off += cfg.n_layer
blob_len, off = u32(off)
blob = data[off:off + blob_len]
off += blob_len
def b32(o): # 在 blob 内读取
return struct.unpack_from(_M + "I", blob, o)[0], o + 4
boff = 0
vocab = []
for _ in range(cfg.vocab_size):
tl, boff = b32(boff)
vocab.append(blob[boff:boff + tl])
boff += tl
merges = []
for _ in range(cfg.num_merges):
a, boff = b32(boff)
b, boff = b32(boff)
merges.append((a, b))
n_t, off = u32(off)
for _ in range(n_t):
nl, off = u32(off)
name = data[off:off + nl].decode("utf-8")
off += nl
rows, off = u32(off)
cols, off = u32(off)
n = rows * cols
if dtype == 0:
arr = np.frombuffer(data, dtype=np.float32, count=n, offset=off).reshape(rows, cols).copy()
off += n * 4
elif dtype == 1:
arr = np.frombuffer(data, dtype=np.float16, count=n, offset=off).astype(np.float32).reshape(rows, cols).copy()
off += n * 2
elif dtype == 2:
nb = (n + Q8_BLOCK - 1) // Q8_BLOCK
vals = np.empty(n, dtype=np.float32)
po = off
k = 0
for _ in range(nb):
scale = np.frombuffer(data, dtype=np.float16, count=1, offset=po).astype(np.float32)[0]
po += 2
cnt = min(Q8_BLOCK, n - k)
q = np.frombuffer(data, dtype=np.int8, count=cnt, offset=po)
po += cnt
vals[k:k + cnt] = q.astype(np.float32) * (scale / 127.0)
k += cnt
arr = vals.reshape(rows, cols).copy()
off += nb * (2 + Q8_BLOCK) # 每块 2 字节 scale + 32 字节 q
else:
raise ValueError(f"未知 dtype {dtype}")
tensors[name] = arr
return cfg, vocab, merges, tensors
def pap_quant(src: str, dst: str, dtype: int) -> None:
"""把已有 .pap 转成另一种 dtype(不改变词表/结构)。dtype: 1=fp16, 2=q8。"""
cfg, vocab, merges, tensors = read_pap(src)
save_pap(dst, cfg, vocab, merges, tensors, dtype=dtype)
+38
查看文件
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""把 .pap 模型转成更小的 dtype(能力基本不变)。
用法:
/opt/pap-venv/bin/python platform/quant_pap.py \
--src models/moe-0.1b.pap --dst models/moe-0.1b-fp16.pap --dtype fp16
--dtype fp16: f32 -> 半精度, 体积约减半 (437MB -> ~219MB), 能力几乎不变
--dtype q8 : f32 -> int8 块量化(每32个共享fp16 scale), 约 437/4 (~110MB), 损失略大
"""
from __future__ import annotations
import argparse
import os
import pap_format
DTYPES = {"f32": 0, "fp16": 1, "q8": 2}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--src", required=True)
ap.add_argument("--dst", required=True)
ap.add_argument("--dtype", default="fp16", choices=DTYPES.keys())
args = ap.parse_args()
dst_dtype = DTYPES[args.dtype]
pap_format.pap_quant(args.src, args.dst, dst_dtype)
in_sz = os.path.getsize(args.src)
out_sz = os.path.getsize(args.dst)
print(f"已转换: {args.src} ({in_sz/1e6:.1f} MB) -> {args.dst} ({out_sz/1e6:.1f} MB)")
print(f" 压缩比 {in_sz/out_sz:.2f}x dtype={args.dtype} (code {dst_dtype})")
if __name__ == "__main__":
main()
+123
查看文件
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""
ParlzAIPlatformPAP — byte-level BPE tokenizer.
与 C 端 (src/model/tokenizer.c) 完全一致的算法:
- id 0..255 = 单个字节 bytes([b])
- id 256+ = BPE 合并产生的 token (vocab[base+i])
- merges[i] = (a, b), 合并后得到 id = 256 + i
编码采用“全局最小 rank 优先”的 BPE(与 C 端一致),用堆实现 O(n log n)。
"""
from __future__ import annotations
import heapq
from typing import List, Tuple
def train_bpe(text: str, vocab_size: int) -> Tuple[List[bytes], List[Tuple[int, int]]]:
"""在字节序列上训练 BPE,返回 (vocab, merges)。"""
raw = text.encode("utf-8")
ids: List[int] = list(raw)
vocab: List[bytes] = [bytes([b]) for b in range(256)]
def get_stats(ids: List[int]) -> dict:
st: dict = {}
for i in range(len(ids) - 1):
pair = (ids[i], ids[i + 1])
st[pair] = st.get(pair, 0) + 1
return st
merges: List[Tuple[int, int]] = []
while len(vocab) < vocab_size:
stats = get_stats(ids)
if not stats:
break
best = max(stats.items(), key=lambda kv: (kv[1], -kv[0][0], -kv[0][1]))[0]
(a, b) = best
new_id = len(vocab)
vocab.append(vocab[a] + vocab[b])
merges.append((a, b))
new_ids: List[int] = []
i = 0
while i < len(ids):
if i < len(ids) - 1 and ids[i] == a and ids[i + 1] == b:
new_ids.append(new_id)
i += 2
else:
new_ids.append(ids[i])
i += 1
ids = new_ids
return vocab, merges
def build_ranks(merges: List[Tuple[int, int]]) -> dict:
return {pair: i for i, pair in enumerate(merges)}
def encode(text: str, vocab_size: int, merges: List[Tuple[int, int]],
bos: bool = False, eos: bool = False, bos_id: int = 0, eos_id: int = 0) -> List[int]:
"""全局最小 rank 的 BPE 编码;结果与 C 端 tokenizer_encode 一致。"""
ranks = {pair: i for i, pair in enumerate(merges)}
if not ranks:
ids = list(text.encode("utf-8"))
else:
ids = _bpe_merge(list(text.encode("utf-8")), ranks)
if bos:
ids = [bos_id] + ids if bos_id != 0 else ids
if eos:
ids = ids + [eos_id]
return ids
def _bpe_merge(ids: List[int], ranks: dict) -> List[int]:
n = len(ids)
if n < 2:
return ids
base = 256
prev = [i - 1 for i in range(n)]
nxt = [i + 1 for i in range(n)]
nxt[n - 1] = -1
alive = [True] * n
heap: List[Tuple[int, int]] = []
for i in range(n - 1):
r = ranks.get((ids[i], ids[i + 1]))
if r is not None:
heapq.heappush(heap, (r, i))
while heap:
rank, i = heapq.heappop(heap)
if not alive[i]:
continue
j = nxt[i]
if j < 0:
continue
cur = ranks.get((ids[i], ids[j]))
if cur != rank: # 过期条目:隔壁合并改变了该 pair
if cur is not None:
heapq.heappush(heap, (cur, i))
continue
# 合并 i,j -> i 保留,j 标记死亡
ids[i] = base + rank
alive[j] = False
k = nxt[j]
nxt[i] = k
if k >= 0:
prev[k] = i
# 受影响的两个关节: (prev[i], i) 与 (i, k)
pj = prev[i]
if pj >= 0 and alive[pj]:
r = ranks.get((ids[pj], ids[i]))
if r is not None:
heapq.heappush(heap, (r, pj))
if k >= 0:
r = ranks.get((ids[i], ids[k]))
if r is not None:
heapq.heappush(heap, (r, i))
return [ids[i] for i in range(n) if alive[i]]
def decode(ids: List[int], vocab: List[bytes]) -> str:
buf = b"".join(vocab[i] for i in ids if 0 <= i < len(vocab))
return buf.decode("utf-8", errors="replace")
+180
查看文件
@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
"""
ParlzAIPlatformPAP — 训练器。
从 (可选的) 语料文件训练一个 GPT+MoE 模型,并导出为 ParlzMAI 可运行的 .pap 文件。
用法(在 WSL 内,使用含 torch 的 venv:
/opt/pap-venv/bin/python platform/train.py --data data/tinyshakespeare.txt \
--out models/moe-0.1b.pap --vocab 2048 --layers 6 --d_model 512 --heads 8 \
--experts 16 --top_k 2 --d_expert 1024 --max_seq 256 --steps 1500 --batch 8
"""
from __future__ import annotations
import argparse
import math
import os
import time
import torch
import tokenizer as tok
import pap_format
from model import ParlzGPTMoE, export_tensors
def build_config(vocab_size, n_layer, d_model, n_head, d_ff, moe_n_experts,
moe_top_k, d_expert, max_seq, eps, moe_every):
cfg = {
"vocab_size": vocab_size,
"n_layer": n_layer,
"d_model": d_model,
"n_head": n_head,
"d_ff": d_ff,
"moe_n_experts": moe_n_experts,
"moe_top_k": moe_top_k,
"d_expert": d_expert,
"max_seq_len": max_seq,
"rmsnorm_eps": eps,
# 每层是否 MoE;moe_every=1 表示全部 MoE,=2 表示隔层
"moe_mask": [1 if (l % moe_every == 0) else 0 for l in range(n_layer)],
}
return cfg
def count_params(model):
return sum(p.numel() for p in model.parameters())
def get_batch(data_t, start, block_size, batch_size, device):
"""取一个 batch: x = [batch, block_size], y 右移一位。"""
ix = torch.randint(0, data_t.numel() - block_size, (batch_size,))
x = torch.stack([data_t[i:i + block_size] for i in ix])
y = torch.stack([data_t[i + 1:i + 1 + block_size] for i in ix])
return x.to(device), y.to(device)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data", default="data/tinyshakespeare.txt")
ap.add_argument("--out", default="models/moe-0.1b.pap")
ap.add_argument("--vocab", type=int, default=2048)
ap.add_argument("--layers", type=int, default=6)
ap.add_argument("--d_model", type=int, default=512)
ap.add_argument("--heads", type=int, default=8)
ap.add_argument("--d_ff", type=int, default=2048)
ap.add_argument("--experts", type=int, default=16)
ap.add_argument("--top_k", type=int, default=2)
ap.add_argument("--d_expert", type=int, default=1024)
ap.add_argument("--max_seq", type=int, default=256)
ap.add_argument("--moe_every", type=int, default=1)
ap.add_argument("--eps", type=float, default=1e-5)
ap.add_argument("--steps", type=int, default=1500)
ap.add_argument("--batch", type=int, default=8)
ap.add_argument("--lr", type=float, default=3e-4)
ap.add_argument("--warmup", type=int, default=100)
ap.add_argument("--eval_every", type=int, default=200)
ap.add_argument("--seed", type=int, default=1337)
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
args = ap.parse_args()
torch.manual_seed(args.seed)
device = args.device
if device.startswith("cuda") and not torch.cuda.is_available():
print("CUDA 不可用,回退到 CPU", flush=True)
device = "cpu"
# ---- 读语料 + BPE(带缓存) ----
if not os.path.exists(args.data):
raise SystemExit(f"语料不存在: {args.data}")
text = open(args.data, "r", encoding="utf-8").read()
print(f"语料: {len(text)} 字符", flush=True)
cache = args.data + ".papcache"
if os.path.exists(cache):
import pickle as _pk
with open(cache, "rb") as f:
vocab, merges, data_ids = _pk.load(f)
vocab_size = len(vocab)
print(f"加载预处理缓存: vocab={vocab_size} merges={len(merges)}", flush=True)
else:
import pickle as _pk
vocab, merges = tok.train_bpe(text, args.vocab)
vocab_size = len(vocab)
data_ids = tok.encode(text, vocab_size, merges)
with open(cache, "wb") as f:
_pk.dump((vocab, merges, data_ids), f)
print(f"词表: {vocab_size} (字节+合并)", flush=True)
# ---- 构建模型 ----
cfg = build_config(vocab_size, args.layers, args.d_model, args.heads, args.d_ff,
args.experts, args.top_k, args.d_expert, args.max_seq,
args.eps, args.moe_every)
model = ParlzGPTMoE(cfg).to(device)
nparams = count_params(model)
print(f"模型参数: {nparams/1e6:.1f} M", flush=True)
# ---- 数据 ----
data_ids = tok.encode(text, vocab_size, merges)
data_t = torch.tensor(data_ids, dtype=torch.long)
block = args.max_seq
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.1,
betas=(0.9, 0.95))
sched = torch.optim.lr_scheduler.LambdaLR(
optimizer,
lambda step: min((step + 1) / max(args.warmup, 1), 1.0))
model.train()
print(f"开始训练 {args.steps} 步 (device={device}) ...", flush=True)
t0 = time.time()
for step in range(1, args.steps + 1):
x, y = get_batch(data_t, 0, block, args.batch, device)
loss, logits = (None, None)
out = model(x, y)
loss = out[1]
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
sched.step()
if step % args.eval_every == 0 or step == 1:
el = time.time() - t0
lr = optimizer.param_groups[0]["lr"]
print(f" step {step:5d}|\tloss {loss.item():.4f}\tlr {lr:.2e}\t{el:.1f}s", flush=True)
model.eval()
init_ids = tok.encode("The", vocab_size, merges)
gen_ids = model.generate_from_ids(init_ids, 60, temperature=0.8, top_k=40)
gen = tok.decode(gen_ids, vocab)
if gen:
print(f" [sample] {gen}", flush=True)
model.train()
print(f"训练完成,耗时 {time.time()-t0:.1f}s", flush=True)
model.eval()
# ---- 导出 .pap ----
tens = export_tensors(model)
pcfg = pap_format.ModelConfig()
pcfg.vocab_size = vocab_size
pcfg.n_layer = args.layers
pcfg.d_model = args.d_model
pcfg.n_head = args.heads
pcfg.d_ff = args.d_ff
pcfg.moe_n_experts = args.experts
pcfg.moe_top_k = args.top_k
pcfg.d_expert = args.d_expert
pcfg.max_seq_len = args.max_seq
pcfg.num_merges = len(merges)
pcfg.tie_weights = 0
pcfg.rmsnorm_eps = args.eps
pcfg.moe_mask = [int(v) for v in cfg["moe_mask"]]
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
pap_format.save_pap(args.out, pcfg, vocab, merges, tens)
print(f"已导出 .pap -> {args.out} ({os.path.getsize(args.out)/1e6:.1f} MB)", flush=True)
if __name__ == "__main__":
main()
+44
查看文件
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""对拍:torch 贪心 vs C 贪心,验证两端数学一致。"""
from __future__ import annotations
import argparse
import subprocess
import model as model_mod
import tokenizer as tok
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--pap", required=True)
ap.add_argument("--prompt", default="First Citizen:")
ap.add_argument("--n", type=int, default=40)
ap.add_argument("--cbin", default="/mnt/f/ParlzMAI/build/moe-serve")
args = ap.parse_args()
m, vocab, merges = model_mod.load_pap_model(args.pap)
init = tok.encode(args.prompt, m.cfg["vocab_size"], merges)
ids = m.generate_from_ids(init, args.n, temperature=0) # greedy
py_text = tok.decode(ids, vocab)
out = subprocess.run(
[args.cbin, "--model", args.pap, "--prompt", args.prompt,
"--n_tokens", str(args.n), "--temperature", "0", "--top_k", "0"],
capture_output=True)
c_all = out.stdout.decode("utf-8", errors="replace")
idx = c_all.rfind(args.prompt)
c_text = c_all[idx:] if idx >= 0 else c_all
print("=== torch greedy ===")
print(py_text)
print("=== C greedy ===")
print(c_text.strip())
ok = c_text.strip() == py_text.strip()
print(f"\n[MATCH] {'YES' if ok else 'NO'}")
return 0 if ok else 1
if __name__ == "__main__":
import sys
sys.exit(main())
+58
查看文件
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""对拍:llama 架构 C(torch 数学) — 合成随机 MoE 模型 → GGUF → C 加载运行 → 与 torch 贪心 ids 一致。"""
from __future__ import annotations
import argparse
import subprocess
import llama_model as lm
import gguf_format
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="/mnt/f/ParlzMAI/models/_llama_smoke.gguf")
ap.add_argument("--cbin", default="/mnt/f/ParlzMAI/build/moe-llama")
ap.add_argument("--n", type=int, default=8)
ap.add_argument("--init", default="1 42 100")
ap.add_argument("--device", default="cpu")
args = ap.parse_args()
torch = __import__("torch")
torch.manual_seed(1234)
cfg = lm.LlamaConfig(vocab_size=512, n_layer=2, n_embd=128, n_head=8, n_head_kv=4,
ffn_dim=256, n_expert=4, n_expert_used=2, max_seq=24,
rmsnorm_eps=1e-5, rope_theta=10000.0)
model = lm.LlamaMoE(cfg)
tens = lm.export_tensors(model)
# 字节 tokenizer(合成): tokens = chr(b), merges = []
tokens = [chr(b) for b in range(256)]
merges = []
gguf_format.save_llama_gguf(args.out, cfg, tens, gml_type=1, tokens=tokens, merges=merges)
# torch 端:从 GGUF 重建(严格圆回)
m2 = lm.load_llama_from_gguf(args.out, cfg)
m2.to("cpu")
init = [int(x) for x in args.init.split()]
py_ids = m2.generate_from_ids(init, args.n, temperature=0) # 贪心
py_gen = py_ids[len(init):] # 去掉 init 前缀
# C 端
cmd = [args.cbin, args.out, str(args.n)] + [str(x) for x in init]
out = subprocess.run(cmd, capture_output=True, text=True)
c_ids = [int(x) for x in out.stdout.split()]
print("=== torch 贪心 ids ===")
print(py_ids)
print("=== C 贪心 ids ===")
print(c_ids)
ok = py_gen == c_ids
print(f"\n[MATCH] {'YES' if ok else 'NO'}")
return 0 if ok else 1
if __name__ == "__main__":
import sys
sys.exit(main())
+24
查看文件
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# 基准:测 pmai 生成速度(tokens/s 与端到端延迟)
# 用法: scripts/bench.sh <model> [n_tokens]
set -euo pipefail
MODEL="${1:-../models/moe-0.1b-fp16.pap}"
N="${2:-100}"
CDIR="$(cd "$(dirname "$0")/.." && pwd)"
BIN="$CDIR/output/pmai"
echo "== pmai 基准: $MODEL n=$N =="
start=$(date +%s.%N)
"$BIN" "$MODEL" --prompt "The" --n_tokens "$N" --temperature 0 --top_k 0 > /tmp/bench_out.txt
end=$(date +%s.%N)
dt=$(echo "$end - $start" | bc)
tok=$(echo "$N" | bc)
tps=$(echo "scale=2; $tok / $dt" | bc)
echo "耗时: ${dt}s 约 ${tps} tokens/s(含模型加载)"
# 仅生成时间(预热一次)
start=$(date +%s.%N)
"$BIN" "$MODEL" --prompt "The" --n_tokens "$N" --temperature 0 --top_k 0 > /dev/null
end=$(date +%s.%N)
echo "第二次(已缓存): $(echo "$end - $start" | bc)s ~$(echo "scale=2; $N / ($end - $start)" | bc) tokens/s"
+39
查看文件
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""从 HF 镜像下载 GGUF / AI 模型文件。
用法: /opt/pap-venv/bin/python scripts/download_model.py --repo Qwen/Qwen2.5-0.5B-Instruct-GGUF \
--file qwen2.5-0.5b-instruct-q4_k_m.gguf --out models/
"""
import argparse
import os
import sys
import urllib.request
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", required=True, help="HF 仓库(如 ggml-org/stories15M_MOE")
ap.add_argument("--file", required=True, help="仓库内文件名")
ap.add_argument("--out", default="models/", help="输出目录")
ap.add_argument("--base", default="https://hf-mirror.com", help="镜像")
args = ap.parse_args()
url = f"{args.base}/{args.repo}/resolve/main/{args.file}"
os.makedirs(args.out, exist_ok=True)
dst = os.path.join(args.out, os.path.basename(args.file))
print(f"下载 {url} -> {dst}")
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=600) as r, open(dst, "wb") as f:
total = 0
while True:
chunk = r.read(1 << 20)
if not chunk:
break
f.write(chunk)
total += len(chunk)
print(f"\r {total/1e6:.1f} MB", end="")
print(f"\n完成: {dst} ({total/1e6:.1f} MB)")
if __name__ == "__main__":
main()
+12
查看文件
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# 等待 pap-venv 的 torch/numpy 装好(用于 WSL 内训练)
for i in $(seq 1 40); do
if /opt/pap-venv/bin/python -c 'import torch,numpy' >/dev/null 2>&1; then
echo "READY"
/opt/pap-venv/bin/python -c 'import torch,numpy;print("torch",torch.__version__,"cuda",torch.cuda.is_available());print("numpy",numpy.__version__)' 2>&1 | head -3
exit 0
fi
sleep 6
done
echo "NOT_READY_AFTER_240s"
exit 1
+10
查看文件
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# 等训练日志出现关键行:语料/词表/参数/步数
for i in $(seq 1 60); do
if grep -qE "step +1|已导出|Traceback|Error|CUDA" /tmp/pap_train.log 2>/dev/null; then
echo "FOUND_AT_${i}"; break
fi
sleep 5
done
echo "----- tail -----"
tail -30 /tmp/pap_train.log 2>/dev/null | tr -d '\0'
+160
查看文件
@@ -0,0 +1,160 @@
#include "core/matrix.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* 简单可复现的 xorshift RNG(避免 srand 的跨平台差异) */
static uint32_t g_rng = 2463534242u;
static uint32_t next_rng(void) {
uint32_t x = g_rng;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
g_rng = x;
return x;
}
static float uniform01(void) {
return (float)(next_rng() & 0xFFFFFFu) / (float)(1u << 24);
}
MoMat *mat_alloc(int rows, int cols) {
if (rows < 0 || cols < 0) return NULL;
MoMat *m = calloc(1, sizeof(MoMat));
if (!m) return NULL;
m->data = calloc((size_t)rows * (size_t)cols, sizeof(float));
if (!m->data) {
free(m);
return NULL;
}
m->rows = rows;
m->cols = cols;
m->stride = cols;
m->owns = 1;
return m;
}
void mat_free(MoMat *m) {
if (!m) return;
if (m->owns && m->data) free(m->data);
free(m);
}
MoMat *mat_view(MoMat *m, int rows, int cols, int64_t stride, float *data) {
if (!m) return NULL;
m->rows = rows;
m->cols = cols;
m->stride = stride;
m->data = data;
m->owns = 0;
return m;
}
void mat_fill(MoMat *m, float value) {
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
for (int j = 0; j < m->cols; j++) row[j] = value;
}
}
void mat_randn(MoMat *m, unsigned seed, float stddev) {
if (seed) g_rng = seed;
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
for (int j = 0; j < m->cols; j++) {
/* Box-Muller:两个均匀 -> 两个高斯,用其中一个 */
float u1 = uniform01();
if (u1 <= 0.0f) u1 = 1e-9f;
float u2 = uniform01();
float z = sqrtf(-2.0f * logf(u1)) * cosf(2.0f * 3.14159265358979323846f * u2);
row[j] = z * stddev;
}
}
}
int mat_mul(const MoMat *a, const MoMat *b, MoMat *out) {
if (!a || !b || !out) return -1;
int M = a->rows;
int K = a->cols;
int N = b->cols;
if (b->rows != K) return -1;
if (out->rows != M || out->cols != N) return -1;
for (int i = 0; i < M; i++) {
const float *pa = a->data + (int64_t)i * a->stride;
float *po = out->data + (int64_t)i * out->stride;
for (int j = 0; j < N; j++) {
float s = 0.0f;
for (int k = 0; k < K; k++) {
s += pa[k] * b->data[(int64_t)k * b->stride + j];
}
po[j] = s;
}
}
return 0;
}
void mat_add(MoMat *a, const MoMat *b) {
if (!a || !b || a->rows != b->rows || a->cols != b->cols) return;
for (int i = 0; i < a->rows; i++) {
float *pa = a->data + (int64_t)i * a->stride;
const float *pb = b->data + (int64_t)i * b->stride;
for (int j = 0; j < a->cols; j++) pa[j] += pb[j];
}
}
void mat_scale(MoMat *m, float factor) {
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
for (int j = 0; j < m->cols; j++) row[j] *= factor;
}
}
MoMat *mat_transpose(const MoMat *a, MoMat *dst) {
if (!a || !dst) return NULL;
if (dst->rows != a->cols || dst->cols != a->rows) return NULL;
for (int i = 0; i < a->rows; i++) {
const float *pa = a->data + (int64_t)i * a->stride;
for (int j = 0; j < a->cols; j++) {
dst->data[(int64_t)j * dst->stride + i] = pa[j];
}
}
return dst;
}
void mat_softmax_rows(MoMat *m) {
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
/* 数值稳定:减去行内最大值 */
float mx = row[0];
for (int j = 1; j < m->cols; j++) if (row[j] > mx) mx = row[j];
float sum = 0.0f;
for (int j = 0; j < m->cols; j++) {
row[j] = expf(row[j] - mx);
sum += row[j];
}
float inv = 1.0f / sum;
for (int j = 0; j < m->cols; j++) row[j] *= inv;
}
}
void mat_relu(MoMat *m) {
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
for (int j = 0; j < m->cols; j++) if (row[j] < 0.0f) row[j] = 0.0f;
}
}
void mat_sigmoid(MoMat *m) {
for (int i = 0; i < m->rows; i++) {
float *row = m->data + (int64_t)i * m->stride;
for (int j = 0; j < m->cols; j++) {
float x = row[j];
row[j] = x >= 0.0f ? (1.0f / (1.0f + expf(-x)))
: (expf(x) / (1.0f + expf(x)));
}
}
}
+62
查看文件
@@ -0,0 +1,62 @@
#ifndef MO_MATRIX_H
#define MO_MATRIX_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 浮点矩阵(行主序)。支持 stride,方便对张量切片做视图而不复制。
* Phase 1 提供基础算子(串行);后续用 OpenMP/BLAS 加速。
*/
typedef struct {
int rows;
int cols;
int64_t stride; /* 相邻两行之间的浮点偏移 */
float *data;
uint8_t owns; /* 是否负责释放 data */
} MoMat;
/* 分配 rows x cols 全 0 矩阵。失败返回 NULL */
MoMat *mat_alloc(int rows, int cols);
/* 释放矩阵(data 仅当 owns 为真时释放) */
void mat_free(MoMat *m);
/* 用外部缓冲构造一个视图(不复制、不释放) */
MoMat *mat_view(MoMat *m, int rows, int cols, int64_t stride, float *data);
/* 填充常量 */
void mat_fill(MoMat *m, float value);
/* 高斯随机初始化(Box-Muller;seed=0 恢复默认序列) */
void mat_randn(MoMat *m, unsigned seed, float stddev);
/* out = a * b。维度须满足 (M,K)x(K,N)->(M,N)。成功返回 0 */
int mat_mul(const MoMat *a, const MoMat *b, MoMat *out);
/* a += b(同尺寸) */
void mat_add(MoMat *a, const MoMat *b);
/* m *= factor */
void mat_scale(MoMat *m, float factor);
/* dst = transpose(a)。dst 尺寸应为 (cols, rows);返回 dst */
MoMat *mat_transpose(const MoMat *a, MoMat *dst);
/* 逐行 softmax(原地) */
void mat_softmax_rows(MoMat *m);
/* ReLU 原地 */
void mat_relu(MoMat *m);
/* Sigmoid 原地 */
void mat_sigmoid(MoMat *m);
#ifdef __cplusplus
}
#endif
#endif /* MO_MATRIX_H */
+95
查看文件
@@ -0,0 +1,95 @@
#include "core/tensor.h"
#include <stdlib.h>
#include <string.h>
static int64_t prod_dims(int ndim, const int64_t *shape) {
int64_t p = 1;
for (int i = 0; i < ndim; i++) p *= shape[i];
return p;
}
static void compute_strides(int ndim, const int64_t *shape, int64_t *strides) {
int64_t s = 1;
for (int i = ndim - 1; i >= 0; i--) {
strides[i] = s;
s *= shape[i];
}
}
MoTensor *tensor_create(int ndim, const int64_t *shape) {
if (ndim <= 0 || !shape) return NULL;
MoTensor *t = calloc(1, sizeof(MoTensor));
if (!t) return NULL;
t->ndim = ndim;
t->shape = malloc((size_t)ndim * sizeof(int64_t));
t->strides = malloc((size_t)ndim * sizeof(int64_t));
if (!t->shape || !t->strides) {
free(t->shape);
free(t->strides);
free(t);
return NULL;
}
memcpy(t->shape, shape, (size_t)ndim * sizeof(int64_t));
compute_strides(ndim, shape, t->strides);
t->numel = prod_dims(ndim, shape);
t->data = calloc((size_t)t->numel, sizeof(float));
t->owns = 1;
if (!t->data) {
free(t->shape);
free(t->strides);
free(t);
return NULL;
}
return t;
}
MoTensor *tensor_create_zeros(int ndim, const int64_t *shape) {
return tensor_create(ndim, shape); /* calloc 已将 data 清零 */
}
void tensor_free(MoTensor *t) {
if (!t) return;
if (t->owns && t->data) free(t->data);
free(t->shape);
free(t->strides);
free(t);
}
MoTensor *tensor_clone(const MoTensor *t) {
if (!t) return NULL;
MoTensor *c = tensor_create(t->ndim, t->shape);
if (!c) return NULL;
memcpy(c->data, t->data, (size_t)t->numel * sizeof(float));
return c;
}
void tensor_fill(MoTensor *t, float value) {
if (!t) return;
for (int64_t i = 0; i < t->numel; i++) t->data[i] = value;
}
int64_t tensor_numel(const MoTensor *t) { return t ? t->numel : 0; }
int64_t tensor_size(const MoTensor *t, int dim) { return t ? t->shape[dim] : 0; }
int64_t tensor_offset(const MoTensor *t, const int64_t *indices) {
int64_t off = 0;
for (int i = 0; i < t->ndim; i++) {
off += indices[i] * t->strides[i];
}
return off;
}
MoTensor *tensor_reshape(MoTensor *t, int ndim, const int64_t *shape) {
if (!t || ndim <= 0 || !shape) return NULL;
if (prod_dims(ndim, shape) != t->numel) return NULL; /* 元素总数不一致 */
memcpy(t->shape, shape, (size_t)ndim * sizeof(int64_t));
compute_strides(ndim, shape, t->strides);
t->ndim = ndim;
return t; /* 就地改写,返回同指针 */
}
+52
查看文件
@@ -0,0 +1,52 @@
#ifndef MO_TENSOR_H
#define MO_TENSOR_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 多维张量。行主序(row-major)连续存储,浮点单精度。
* 参考 llama2.c / Qwen 的分层思路,Phase 1 先做基础结构与访问。
*/
typedef struct {
int ndim; /* 维度数量 */
int64_t *shape; /* 每个维度大小,长度 ndim */
int64_t *strides; /* 每个维度的元素步长(行主序),长度 ndim */
int64_t numel; /* 元素总数 = prod(shape) */
float *data; /* 连续元素缓冲(行主序) */
uint8_t owns; /* data 是否为本对象所有(由本对象负责释放) */
} MoTensor;
/* 创建全 0 张量(赋值阻塞维度语义)。失败返回 NULL */
MoTensor *tensor_create(int ndim, const int64_t *shape);
MoTensor *tensor_create_zeros(int ndim, const int64_t *shape);
/* 释放张量(data 仅当 owns 为真时释放) */
void tensor_free(MoTensor *t);
/* 深拷贝(新的 data 缓冲区) */
MoTensor *tensor_clone(const MoTensor *t);
/* 用标量填充 */
void tensor_fill(MoTensor *t, float value);
/* 元素总数 */
int64_t tensor_numel(const MoTensor *t);
/* 某维大小 */
int64_t tensor_size(const MoTensor *t, int dim);
/* 计算给定下标(长度 ndim)的线性偏移 */
int64_t tensor_offset(const MoTensor *t, const int64_t *indices);
/* 就地 reshape 视图(不复制数据)。元素总数必须一致,否则返回 NULL */
MoTensor *tensor_reshape(MoTensor *t, int ndim, const int64_t *shape);
#ifdef __cplusplus
}
#endif
#endif /* MO_TENSOR_H */
+218
查看文件
@@ -0,0 +1,218 @@
#include "core/transformer.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* ---- 微型算子 ---- */
/* y = x / sqrt(mean(x^2)+eps) * w */
static void rmsnorm(const float *x, const float *w, float eps, int d, float *y) {
float sq = 0.0f;
for (int i = 0; i < d; i++) sq += x[i] * x[i];
float inv = 1.0f / sqrtf(sq / (float)d + eps);
for (int i = 0; i < d; i++) y[i] = x[i] * inv * w[i];
}
/* y[cols] = x[k] @ W[k,cols](行主序 W */
static void matvec(const float *x, const float *W, int k, int cols, float *y) {
for (int j = 0; j < cols; j++) {
float s = 0.0f;
for (int i = 0; i < k; i++) s += x[i] * W[i * cols + j];
y[j] = s;
}
}
/* GELU (tanh 近似),与 Python 一致 */
static void gelu(float *x, int n) {
const float c = 0.7978845608028654f;
const float a = 0.044715f;
for (int i = 0; i < n; i++) {
float v = x[i];
x[i] = 0.5f * v * (1.0f + tanhf(c * (v + a * v * v * v)));
}
}
/* ---- KV cache ---- */
void kv_init(KVState *kv, const Model *m) {
memset(kv, 0, sizeof(*kv));
kv->max_seq = m->config.max_seq_len;
kv->n_layer = m->config.n_layer;
size_t per = (size_t)kv->max_seq * m->config.n_head * m->config.head_dim;
kv->k_cache = calloc((size_t)kv->n_layer, sizeof(float *));
kv->v_cache = calloc((size_t)kv->n_layer, sizeof(float *));
for (int l = 0; l < kv->n_layer; l++) {
kv->k_cache[l] = malloc(per * sizeof(float));
kv->v_cache[l] = malloc(per * sizeof(float));
}
kv->seq_len = 0;
}
void kv_free(KVState *kv) {
if (!kv) return;
for (int l = 0; l < kv->n_layer; l++) {
free(kv->k_cache[l]);
free(kv->v_cache[l]);
}
free(kv->k_cache);
free(kv->v_cache);
memset(kv, 0, sizeof(*kv));
}
void kv_reset(KVState *kv) { kv->seq_len = 0; }
void moe_embed(const Model *m, int token, int pos, float *x) {
int d = m->config.d_model;
const float *te = m->wte + (size_t)token * d;
const float *pe = m->wpe + (size_t)pos * d;
for (int i = 0; i < d; i++) x[i] = te[i] + pe[i];
}
/* ---- 稠密 FFNx -> gelu(x@w1) @ w2 ---- */
static void dense_ffn(const Model *m, int l, const float *x, float *out) {
int d = m->config.d_model;
int f = m->config.d_ff;
float *mid = malloc((size_t)f * sizeof(float));
matvec(x, m->ffn_w1[l], d, f, mid);
gelu(mid, f);
matvec(mid, m->ffn_w2[l], f, d, out);
free(mid);
}
/* ---- MoErouter softmax + top-k 专家加权和 ---- */
static void moe_ffn(const Model *m, int l, const float *x, float *out) {
int d = m->config.d_model;
int ne = m->config.moe_n_experts;
int topk = m->config.moe_top_k;
int de = m->config.d_expert;
/* router logits & softmax */
float *probs = malloc((size_t)ne * sizeof(float));
matvec(x, m->router[l], d, ne, probs);
float mx = probs[0];
for (int e = 1; e < ne; e++) if (probs[e] > mx) mx = probs[e];
float sum = 0.0f;
for (int e = 0; e < ne; e++) { probs[e] = expf(probs[e] - mx); sum += probs[e]; }
float inv = 1.0f / sum;
for (int e = 0; e < ne; e++) probs[e] *= inv;
/* top-k 选择(在概率上做简单选择)*/
int *idx = malloc((size_t)topk * sizeof(int));
for (int t = 0; t < topk; t++) {
int best = -1;
float bestp = -1.0f;
for (int e = 0; e < ne; e++) {
int taken = 0;
for (int q = 0; q < t; q++) if (idx[q] == e) { taken = 1; break; }
if (!taken && probs[e] > bestp) { bestp = probs[e]; best = e; }
}
idx[t] = best;
}
float *mid = malloc((size_t)de * sizeof(float));
float *eo = malloc((size_t)d * sizeof(float));
for (int i = 0; i < d; i++) out[i] = 0.0f;
for (int t = 0; t < topk; t++) {
int e = idx[t];
matvec(x, m->exp_w1[l][e], d, de, mid);
gelu(mid, de);
matvec(mid, m->exp_w2[l][e], de, d, eo);
float w = probs[e];
for (int i = 0; i < d; i++) out[i] += w * eo[i];
}
free(probs);
free(idx);
free(mid);
free(eo);
}
/* ---- 单位置前向 ---- */
void moe_forward(const Model *m, KVState *kv, int pos, const float *x, float *logits) {
const ModelConfig *c = &m->config;
int d = c->d_model, nh = c->n_head, hd = c->head_dim;
int L = c->n_layer, V = c->vocab_size;
float *a = malloc((size_t)d * sizeof(float)); /* norm out */
float *q = malloc((size_t)(nh * hd) * sizeof(float));
float *k = malloc((size_t)(nh * hd) * sizeof(float));
float *v = malloc((size_t)(nh * hd) * sizeof(float));
float *att = malloc((size_t)d * sizeof(float)); /* 注意力输出(暂存 o 前) */
float *o = malloc((size_t)d * sizeof(float));
float *ffn = malloc((size_t)d * sizeof(float));
float *scores = malloc((size_t)(kv->max_seq) * sizeof(float));
float *h = malloc((size_t)d * sizeof(float));
memcpy(h, x, (size_t)d * sizeof(float));
float inv_sqrt = 1.0f / sqrtf((float)hd);
for (int l = 0; l < L; l++) {
/* 1) norm1 */
rmsnorm(h, m->l_norm1[l], c->rmsnorm_eps, d, a);
/* 2) qkv */
matvec(a, m->wq[l], d, d, q);
matvec(a, m->wk[l], d, d, k);
matvec(a, m->wv[l], d, d, v);
/* 存 cacheindex = pos*(nh*hd) + h*hd + t */
{
float *kc = kv->k_cache[l] + (size_t)pos * nh * hd;
float *vc = kv->v_cache[l] + (size_t)pos * nh * hd;
memcpy(kc, k, (size_t)(nh * hd) * sizeof(float));
memcpy(vc, v, (size_t)(nh * hd) * sizeof(float));
}
/* 3) 自注意力(因果),每个 head 独立 */
for (int hh = 0; hh < nh; hh++) {
const float *qh = q + (size_t)hh * hd;
/* scores[j] = (qh . k_j)/sqrt(hd) for j in 0..pos */
for (int j = 0; j <= pos; j++) {
const float *kj = kv->k_cache[l] + (size_t)j * nh * hd + (size_t)hh * hd;
float s = 0.0f;
for (int t = 0; t < hd; t++) s += qh[t] * kj[t];
scores[j] = s * inv_sqrt;
}
float mxs = scores[0];
for (int j = 1; j <= pos; j++) if (scores[j] > mxs) mxs = scores[j];
float ss = 0.0f;
for (int j = 0; j <= pos; j++) { scores[j] = expf(scores[j] - mxs); ss += scores[j]; }
float is = 1.0f / ss;
for (int j = 0; j <= pos; j++) scores[j] *= is;
for (int t = 0; t < hd; t++) {
float acc = 0.0f;
for (int j = 0; j <= pos; j++) {
const float *vj = kv->v_cache[l] + (size_t)j * nh * hd + (size_t)hh * hd;
acc += scores[j] * vj[t];
}
att[(size_t)hh * hd + t] = acc;
}
}
/* 4) o = att @ wo */
matvec(att, m->wo[l], d, d, o);
/* 5) residual */
for (int i = 0; i < d; i++) h[i] += o[i];
/* 6) norm2 */
rmsnorm(h, m->l_norm2[l], c->rmsnorm_eps, d, a);
/* 7) ffn / moe */
if (c->moe_mask[l]) {
moe_ffn(m, l, a, ffn);
} else {
dense_ffn(m, l, a, ffn);
}
for (int i = 0; i < d; i++) h[i] += ffn[i];
}
/* 最终 norm + lm_head */
rmsnorm(h, m->norm_final, c->rmsnorm_eps, d, a);
matvec(a, m->lm_head, d, V, logits); /* lm_head 为 (d, vocab) 行主序 */
free(a); free(q); free(k); free(v); free(att); free(o);
free(ffn); free(scores); free(h);
}
+36
查看文件
@@ -0,0 +1,36 @@
#ifndef MO_TRANSFORMER_H
#define MO_TRANSFORMER_H
#include <stdint.h>
#include "model/model.h"
#ifdef __cplusplus
extern "C" {
#endif
/* KV 缓存:每层一份,容量 = max_seq * n_head * head_dim */
typedef struct {
int seq_len; /* 已处理位置数 */
int max_seq;
int n_layer;
float **k_cache; /* [layer] */
float **v_cache; /* [layer] */
} KVState;
void kv_init(KVState *kv, const Model *m);
void kv_free(KVState *kv);
void kv_reset(KVState *kv);
/* 计算单个位置 token 的嵌入 x = wte[token] + wpe[pos],写入 x(d_model) */
void moe_embed(const Model *m, int token, int pos, float *x);
/* 对位置 pos 执行一次前向,返回该位置的 logits(vocab)。
会写入 KV cache(pos 处)。调用方负责已就绪的嵌入与 cache 状态。 */
void moe_forward(const Model *m, KVState *kv, int pos, const float *x, float *logits);
#ifdef __cplusplus
}
#endif
#endif /* MO_TRANSFORMER_H */
+115
查看文件
@@ -0,0 +1,115 @@
#include "infer/generate.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "infer/sample.h"
#include "model/tokenizer.h"
#include "utils/logger.h"
/* ---- 流式生成 ---- */
int gen_stream_begin(GenStream *gs, const Model *m, const char *prompt,
int max_new, float temperature, int top_k) {
memset(gs, 0, sizeof(*gs));
gs->m = m;
gs->max_new = max_new;
gs->temperature = temperature;
gs->top_k = top_k;
const int max_seq = m->config.max_seq_len;
gs->ids = malloc((size_t)(max_seq + 16) * sizeof(int));
if (!gs->ids) return -1;
gs->ptoks = tokenizer_encode(&m->tok, prompt, gs->ids, max_seq + 16);
if (gs->ptoks < 0) { free(gs->ids); gs->ids = NULL; return -1; }
if (gs->ptoks > max_seq) gs->ptoks = max_seq;
gs->logits = malloc((size_t)m->config.vocab_size * sizeof(float));
gs->x = malloc((size_t)m->config.d_model * sizeof(float));
if (!gs->logits || !gs->x) return -1;
kv_init(&gs->kv, m);
/* 预填充 prompt,建立 KV cache */
for (int i = 0; i < gs->ptoks; i++) {
moe_embed(m, gs->ids[i], i, gs->x);
moe_forward(m, &gs->kv, i, gs->x, gs->logits);
}
gs->pos = gs->ptoks;
return 0;
}
int gen_stream_next(GenStream *gs, char *out_buf, size_t out_sz) {
const Model *m = gs->m;
if (gs->done) return 0;
if (gs->emitted >= gs->max_new || gs->pos >= m->config.max_seq_len) {
gs->done = 1;
return 0;
}
int next = sample_top_k(gs->logits, m->config.vocab_size, gs->temperature, gs->top_k);
tokenizer_decode(&m->tok, &next, 1, out_buf, (int)out_sz);
moe_embed(m, next, gs->pos, gs->x);
moe_forward(m, &gs->kv, gs->pos, gs->x, gs->logits);
gs->pos++;
gs->emitted++;
return 1;
}
void gen_stream_end(GenStream *gs) {
if (!gs) return;
kv_free(&gs->kv);
free(gs->logits);
free(gs->x);
free(gs->ids);
memset(gs, 0, sizeof(*gs));
}
/* ---- 一次性生成(CLI ---- */
int moe_generate(const Model *m, const char *prompt, int max_new_tokens,
float temperature, int top_k) {
GenStream gs;
if (gen_stream_begin(&gs, m, prompt, max_new_tokens, temperature, top_k) < 0) {
MO_LOGE("prompt 编码失败");
return -1;
}
printf("%s", prompt);
fflush(stdout);
char buf[64];
while (gen_stream_next(&gs, buf, sizeof(buf)) == 1) {
printf("%s", buf);
fflush(stdout);
}
printf("\n");
gen_stream_end(&gs);
return 0;
}
char *moe_generate_text(const Model *m, const char *prompt, int max_new_tokens,
float temperature, int top_k) {
GenStream gs;
size_t cap = strlen(prompt) + (size_t)max_new_tokens * 16 + 128;
char *buf = malloc(cap);
if (!buf) return NULL;
size_t w = (size_t)snprintf(buf, cap, "%s", prompt);
if (gen_stream_begin(&gs, m, prompt, max_new_tokens, temperature, top_k) < 0) {
MO_LOGE("prompt 编码失败");
gen_stream_end(&gs);
return buf;
}
char tok[64];
while (gen_stream_next(&gs, tok, sizeof(tok)) == 1) {
size_t tl = strlen(tok);
if (w + tl + 1 > cap) {
cap = cap * 2 + tl + 64;
buf = realloc(buf, cap);
}
memcpy(buf + w, tok, tl);
w += tl;
buf[w] = '\0';
}
gen_stream_end(&gs);
return buf;
}
+49
查看文件
@@ -0,0 +1,49 @@
#ifndef MO_GENERATE_H
#define MO_GENERATE_H
#include <stddef.h>
#include "core/transformer.h"
#include "model/model.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---------- 一次性生成(CLI 用,自回归打印到 stdout ---------- */
int moe_generate(const Model *m, const char *prompt, int max_new_tokens,
float temperature, int top_k);
/* 生成并返回完整文本(prompt + 生成)。调用方 free()。失败返回 strdup(prompt)。 */
char *moe_generate_text(const Model *m, const char *prompt, int max_new_tokens,
float temperature, int top_k);
/* ---------- 流式生成(逐个 token 拉取,供 HTTP/SSE 用) ---------- */
typedef struct {
const Model *m;
KVState kv; /* 内部 KV cache */
float *logits;
float *x;
int *ids; /* prompt token 缓存 */
int ptoks;
int pos;
int emitted; /* 已产出 token 数 */
int max_new;
float temperature;
int top_k;
int done;
char last_buf[64]; /* 最后一个 token 的utf8(避免重复解码) */
} GenStream;
/* 初始化 + 预填充 prompt。成功返回 0 */
int gen_stream_begin(GenStream *gs, const Model *m, const char *prompt,
int max_new, float temperature, int top_k);
/* 拉取下一个 token 文本到 out_buf;返回 1=产出tokens,0=结束,负=出错 */
int gen_stream_next(GenStream *gs, char *out_buf, size_t out_sz);
void gen_stream_end(GenStream *gs);
#ifdef __cplusplus
}
#endif
#endif /* MO_GENERATE_H */
+64
查看文件
@@ -0,0 +1,64 @@
#include "infer/sample.h"
#include <math.h>
#include <stdlib.h>
static uint32_t g_rng = 2463534242u;
void mo_rng_seed(unsigned seed) { g_rng = seed ? seed : 2463534242u; }
static uint32_t next_rng(void) {
uint32_t x = g_rng;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
g_rng = x;
return x;
}
typedef struct { float v; int idx; } Scored;
static int cmp_desc(const void *a, const void *b) {
const Scored *pa = (const Scored *)a, *pb = (const Scored *)b;
if (pa->v > pb->v) return -1;
if (pa->v < pb->v) return 1;
return pa->idx - pb->idx;
}
int sample_top_k(const float *logits, int vocab, float temperature, int top_k) {
if (temperature <= 0.0f) {
/* 贪心 */
int best = 0;
for (int i = 1; i < vocab; i++) if (logits[i] > logits[best]) best = i;
return best;
}
if (top_k <= 0 || top_k > vocab) top_k = vocab;
Scored *sc = malloc((size_t)vocab * sizeof(Scored));
for (int i = 0; i < vocab; i++) {
sc[i].v = logits[i] / temperature;
sc[i].idx = i;
}
qsort(sc, (size_t)vocab, sizeof(Scored), cmp_desc);
/* softmax over 前 top_k 个 */
float mxs = sc[0].v;
float sum = 0.0f;
for (int i = 0; i < top_k; i++) {
sc[i].v = expf(sc[i].v - mxs);
sum += sc[i].v;
}
float target = ((float)(next_rng() & 0xFFFFFFu) / (float)(1u << 24)) * sum;
float acc = 0.0f;
for (int i = 0; i < top_k; i++) {
acc += sc[i].v;
if (target <= acc) {
int idx = sc[i].idx;
free(sc);
return idx;
}
}
int idx = sc[top_k - 1].idx;
free(sc);
return idx;
}
+20
查看文件
@@ -0,0 +1,20 @@
#ifndef MO_SAMPLE_H
#define MO_SAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
void mo_rng_seed(unsigned seed);
/* 依据 logits 采样一个 token id。
temperature logitstop_k xorshift */
int sample_top_k(const float *logits, int vocab, float temperature, int top_k);
#ifdef __cplusplus
}
#endif
#endif /* MO_SAMPLE_H */
+51
查看文件
@@ -0,0 +1,51 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "model/llama.h"
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "用法: pmai-llama <model.gguf> [n_new] [prompt 或 init_id ...]\n");
return 1;
}
LlamaModel m;
if (llama_load(&m, argv[1]) != 0) {
fprintf(stderr, "加载失败: %s\n", argv[1]);
return 2;
}
int n_new = argc > 2 ? atoi(argv[2]) : 10;
int ids[512];
int n = 0;
/* --tok <text>:仅分词,打印 token id 后退出(用于验证分词器) */
if (m.is_loaded && m.tok.vocab_size > 0 && argc >= 5 && strcmp(argv[3], "--tok") == 0) {
n = llama_tokenize(&m, argv[4], ids, 512);
for (int i = 0; i < n; i++) printf("%d", ids[i]), putchar(i + 1 < n ? ' ' : '\n');
llama_free(&m);
return 0;
}
/* 有分词器且第 3 个参数是字符串 -> 当作 prompt 编码 */
if (m.is_loaded && m.tok.vocab_size > 0 && argc >= 4) {
n = llama_tokenize(&m, argv[3], ids, 512);
if (n <= 0) { ids[0] = 1; n = 1; }
} else {
for (int i = 3; i < argc && n < 512; i++) ids[n++] = atoi(argv[i]);
if (n == 0) ids[n++] = 1;
}
int out[512];
int p = llama_generate(&m, ids, n, n_new, 0.0f, 0, out);
if (m.tok.vocab_size > 0) {
char buf[4096];
int blen = llama_detokenize(&m, out, p, buf, sizeof(buf));
fwrite(buf, 1, (size_t)(blen < 0 ? 0 : blen), stdout);
printf("\n");
} else {
for (int i = 0; i < p; i++) printf("%d", out[i]), putchar(i + 1 < p ? ' ' : '\n');
}
llama_free(&m);
return 0;
}
+275
查看文件
@@ -0,0 +1,275 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <time.h>
#include "infer/generate.h"
#include "infer/sample.h"
#include "model/gguf.h"
#include "model/model.h"
#ifdef MOE_HTTP
#include "server/api.h"
#endif
#include "utils/logger.h"
static void usage(void) {
printf("用法: pmai --model <file.pap|file.gguf> [选项]\n"
" --model <path> 模型文件(必填;.gguf 自动识别)\n"
" --inspect <path> 查看任意 GGUF 的元数据/张量表\n"
" --prompt <text> 提示词(默认: \"The\"\n"
" --n_tokens <N> 生成 token 数(默认 100\n"
" --temperature <T> 采样温度(默认 0.8,<=0 为贪心)\n"
" --top_k <K> top-k 采样(默认 40,<=0 不限制)\n"
" --seed <S> 随机种子\n"
" --interactive 交互式对话(输入一行→生成→继续,exit/quit 退出)\n"
" --chat 聊天模式(User:/Assistant: 格式,带上下文,存记录)\n"
" --output <file> 把生成文本写到文件(默认在 output/ 目录)\n"
" --serve 以 HTTP 服务方式运行(需 --model\n"
" --port <P> HTTP 端口(默认 11434\n"
" --log-level <lvl> debug/info/warn/error\n");
}
static int has_gguf_magic(const char *path) {
const char m[4] = {'G', 'G', 'U', 'F'};
FILE *f = fopen(path, "rb");
if (!f) return 0;
char b[4];
int n = (int)fread(b, 1, 4, f);
fclose(f);
return (n == 4 && memcmp(b, m, 4) == 0);
}
static int arg_i(const char *s, int def) { return s ? atoi(s) : def; }
static float arg_f(const char *s, float def) { return s ? (float)atof(s) : def; }
static int write_text_file(const char *path, const char *content) {
FILE *f = fopen(path, "wb");
if (!f) return -1;
fwrite(content, 1, strlen(content), f);
fclose(f);
return 0;
}
/* 从简单 key: value 的 config 文件加载默认值(host/port/model/n_tokens/temperature/top_k */
static void load_config(const char *path, unsigned *port, int *n_tokens, float *temperature,
int *top_k, char *model, size_t model_cap) {
FILE *f = fopen(path, "r");
if (!f) return;
char line[512];
while (fgets(line, sizeof(line), f)) {
char *c = strchr(line, '#');
if (c) *c = '\0';
if ((line[0] == '\n' || line[0] == '\r') && !line[1]) continue;
char key[160] = {0}, val[384] = {0};
if (sscanf(line, "%159[^:]: %383[^\n]", key, val) != 2) continue;
for (char *p = key; *p; p++) if (*p == ' ' || *p == '\t') { *p = '\0'; break; }
if (!strcmp(key, "port")) *port = (unsigned)atoi(val);
else if (!strcmp(key, "model")) snprintf(model, model_cap, "%s", val);
else if (!strcmp(key, "n_tokens")) *n_tokens = atoi(val);
else if (!strcmp(key, "temperature")) *temperature = (float)atof(val);
else if (!strcmp(key, "top_k")) *top_k = atoi(val);
}
fclose(f);
}
/* 聊天式生成:从 prompt 前缀开始生成,遇到下一个 "\nUser:"(下一轮用户话)停止,返回助手回复。调用方 free() */
static char *run_chat(Model *m, const char *prompt, int max_new, float temperature, int top_k) {
GenStream gs;
if (gen_stream_begin(&gs, m, prompt, max_new, temperature, top_k) < 0) return strdup("");
size_t cap = (size_t)max_new * 16 + 256;
char *buf = malloc(cap);
size_t w = 0;
buf[0] = '\0';
char tok[64];
while (gen_stream_next(&gs, tok, sizeof(tok)) == 1) {
size_t tl = strlen(tok);
if (w + tl + 1 > cap) {
cap = cap * 2 + tl + 64;
buf = realloc(buf, cap);
}
memcpy(buf + w, tok, tl);
w += tl;
buf[w] = '\0';
if (strstr(buf, "\nUser:")) break; /* 已生成到下一轮用户话,停止 */
}
gen_stream_end(&gs);
char *np = strstr(buf, "\nUser:");
if (np) *np = '\0'; /* 截掉后面的下一轮 */
return buf;
}
int main(int argc, char **argv) {
const char *model = NULL, *prompt = NULL, *loglvl = NULL, *inspect = NULL;
int n_tokens = 100;
float temperature = 0.8f;
int top_k = 40;
int seed = 0;
int serve = 0;
int interactive = 0;
int chat = 0;
unsigned port = 11434;
const char *output = NULL;
const char *config = NULL;
char config_model[512] = "";
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--model") && i + 1 < argc) model = argv[++i];
else if (!strcmp(argv[i], "--inspect") && i + 1 < argc) inspect = argv[++i];
else if (!strcmp(argv[i], "--prompt") && i + 1 < argc) prompt = argv[++i];
else if (!strcmp(argv[i], "--n_tokens") && i + 1 < argc) n_tokens = arg_i(argv[++i], 100);
else if (!strcmp(argv[i], "--temperature") && i + 1 < argc) temperature = arg_f(argv[++i], 0.8f);
else if (!strcmp(argv[i], "--top_k") && i + 1 < argc) top_k = arg_i(argv[++i], 40);
else if (!strcmp(argv[i], "--seed") && i + 1 < argc) seed = arg_i(argv[++i], 0);
else if (!strcmp(argv[i], "--interactive")) interactive = 1;
else if (!strcmp(argv[i], "--chat")) chat = 1;
else if (!strcmp(argv[i], "--output") && i + 1 < argc) output = argv[++i];
else if (!strcmp(argv[i], "--config") && i + 1 < argc) config = argv[++i];
else if (!strcmp(argv[i], "--serve")) serve = 1;
else if (!strcmp(argv[i], "--port") && i + 1 < argc) port = (unsigned)arg_i(argv[++i], 11434);
else if (!strcmp(argv[i], "--log-level") && i + 1 < argc) loglvl = argv[++i];
else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { usage(); return 0; }
}
/* 便利:未用 --model 时,把第一个非 '-' 位置参数当作模型路径 */
if (config) {
load_config(config, &port, &n_tokens, &temperature, &top_k, config_model, sizeof(config_model));
if (!model && config_model[0]) model = config_model;
}
if (!model && argc > 1 && argv[1][0] != '-') model = argv[1];
if (loglvl) {
if (!strcasecmp(loglvl, "debug")) mo_log_set_level(MO_LOG_DEBUG);
else if (!strcasecmp(loglvl, "info")) mo_log_set_level(MO_LOG_INFO);
else if (!strcasecmp(loglvl, "warn")) mo_log_set_level(MO_LOG_WARN);
else if (!strcasecmp(loglvl, "error")) mo_log_set_level(MO_LOG_ERROR);
}
if (inspect) {
Gguf g;
if (gguf_open(&g, inspect) != 0) { MO_LOGE("GGUF 打开失败: %s", inspect); return 2; }
gguf_inspect(&g, inspect, stdout);
gguf_close(&g);
return 0;
}
if (!model) { usage(); return 1; }
mo_rng_seed((unsigned)seed);
Model m;
int rc = has_gguf_magic(model) ? model_load_gguf(&m, model) : model_load(&m, model);
if (rc != 0) {
MO_LOGE("加载模型失败(%d): %s", rc, model);
return 2;
}
ModelConfig *c = &m.config;
MO_LOGI("已加载模型: %s", model);
MO_LOGI(" config: vocab=%d layers=%d d_model=%d heads=%d max_seq=%d",
c->vocab_size, c->n_layer, c->d_model, c->n_head, c->max_seq_len);
MO_LOGI(" MoE: experts=%d top_k=%d d_expert=%d",
c->moe_n_experts, c->moe_top_k, c->d_expert);
MO_LOGI(" params: %lld", (long long)model_param_count(&m));
if (!prompt) prompt = "The";
MO_LOGI("生成中: n_tokens=%d temperature=%.2f top_k=%d", n_tokens, temperature, top_k);
if (serve) {
#ifdef MOE_HTTP
moe_server_run(&m, model, port);
#else
MO_LOGE("未启用 HTTP 服务(缺 libmicrohttpd/jansson,用 -DBUILD_HTTP=ON 重建)");
(void)port;
#endif
model_free(&m);
return 0;
}
if (chat) {
mkdir("output", 0755);
char outpath[512];
if (output) snprintf(outpath, sizeof(outpath), "%s", output);
else snprintf(outpath, sizeof(outpath), "output/chat-%ld.txt", (long)time(NULL));
FILE *logf = fopen(outpath, "wb");
if (logf) fprintf(logf, "# pmai 聊天记录\n\n");
MO_LOGI("聊天模式已启动。输入内容即对话;exit/quit 退出。记录保存到 %s", outpath);
size_t hcap = 4096, hlen = 0;
char *hist = calloc(1, hcap);
char line[4096];
for (;;) {
printf("\n你: ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
size_t len = strlen(line);
while (len && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0';
if (!len) continue;
if (!strcmp(line, "exit") || !strcmp(line, "quit")) break;
/* prompt = history + "User: <line>\nAssistant:" */
size_t need = hlen + len + 64;
if (need > hcap) { while (need > hcap) hcap *= 2; hist = realloc(hist, hcap); }
size_t p = (size_t)snprintf(hist + hlen, hcap - hlen, "User: %s\nAssistant:", line);
hlen += p;
char *reply = run_chat(&m, hist, n_tokens, temperature, top_k);
if (!reply || !reply[0]) { free(reply); reply = strdup("(我还没想好,换句话试试?)"); }
printf("%s\n", reply);
/* 回填历史,供下一轮携带上下文;保留尾部避免超出上下文 */
size_t adds = (size_t)snprintf(hist + hlen, hcap - hlen, "\nAssistant: %s\n", reply ? reply : "");
hlen += adds;
if (hlen > 2048) { size_t keep = 1536; memmove(hist, hist + hlen - keep, keep + 1); hlen = keep; }
if (logf && reply) { fprintf(logf, "User: %s\nAssistant: %s\n\n", line, reply); fflush(logf); }
free(reply);
}
free(hist);
if (logf) fclose(logf);
model_free(&m);
return 0;
}
if (interactive) {
mkdir("output", 0755);
char outpath[512];
if (output) snprintf(outpath, sizeof(outpath), "%s", output);
else snprintf(outpath, sizeof(outpath), "output/chat-%ld.txt", (long)time(NULL));
FILE *logf = fopen(outpath, "wb");
if (logf) fprintf(logf, "# pmai 交互记录\n\n");
MO_LOGI("交互模式已启动。输入一行后回车即生成;输入 exit/quit 退出。对话保存到 %s", outpath);
char line[4096];
for (;;) {
printf("\n>> ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
size_t len = strlen(line);
while (len && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0';
if (!len) continue;
if (!strcmp(line, "exit") || !strcmp(line, "quit")) break;
char *out = moe_generate_text(&m, line, n_tokens, temperature, top_k);
printf("%s\n", out ? out : "");
if (logf && out) { fprintf(logf, ">> %s\n%s\n\n", line, out); fflush(logf); }
free(out);
}
if (logf) fclose(logf);
model_free(&m);
return 0;
}
if (output) {
mkdir("output", 0755);
char *out = moe_generate_text(&m, prompt, n_tokens, temperature, top_k);
int rc = write_text_file(output, out ? out : "");
MO_LOGI("生成已写入 %s (%s)", output, rc == 0 ? "ok" : "fail");
printf("%s\n", out ? out : "");
free(out);
model_free(&m);
return 0;
}
moe_generate(&m, prompt, n_tokens, temperature, top_k);
model_free(&m);
return 0;
}
+16
查看文件
@@ -0,0 +1,16 @@
#include "model/config.h"
#include <stdlib.h>
#include <string.h>
void config_init(ModelConfig *c) {
memset(c, 0, sizeof(*c));
c->head_dim = 0;
c->rmsnorm_eps = 1e-5f;
}
void config_free(ModelConfig *c) {
if (!c) return;
free(c->moe_mask);
c->moe_mask = NULL;
}
+33
查看文件
@@ -0,0 +1,33 @@
#ifndef MO_CONFIG_H
#define MO_CONFIG_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
int vocab_size;
int n_layer;
int d_model;
int n_head;
int head_dim; /* d_model / n_head */
int d_ff; /* 稠密 FFN 中间层 */
int moe_n_experts;
int moe_top_k;
int d_expert; /* MoE 专家中间层 */
int max_seq_len;
int num_merges;
int n_special;
int tie_weights;
float rmsnorm_eps;
unsigned char *moe_mask; /* n_layer: 1=MoE, 0=dense */
} ModelConfig;
void config_init(ModelConfig *c);
void config_free(ModelConfig *c);
#ifdef __cplusplus
}
#endif
#endif /* MO_CONFIG_H */
+465
查看文件
@@ -0,0 +1,465 @@
#include "model/gguf.h"
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
/* ---- ggml_type 枚举(常用) ---- */
#define GGML_F32 0
#define GGML_F16 1
#define GGML_Q4_0 2
#define GGML_Q5_0 6
#define GGML_Q5_1 7
#define GGML_Q8_0 8
#define GGML_BF16 16
/* ---- 元数据值类型 ---- */
#define GGVAL_UINT8 0
#define GGVAL_INT8 1
#define GGVAL_UINT16 2
#define GGVAL_INT16 3
#define GGVAL_UINT32 4
#define GGVAL_INT32 5
#define GGVAL_FLOAT32 6
#define GGVAL_BOOL 7
#define GGVAL_STRING 8
#define GGVAL_ARRAY 9
#define GGVAL_UINT64 10
#define GGVAL_INT64 11
#define GGVAL_FLOAT64 12
static const unsigned char *gd(const Gguf *g, size_t off) { return g->data + off; }
static uint64_t rd_u64(const unsigned char *p) {
uint64_t v; memcpy(&v, p, 8); return v;
}
static uint32_t rd_u32(const unsigned char *p) {
uint32_t v; memcpy(&v, p, 4); return v;
}
static uint16_t rd_u16(const unsigned char *p) {
uint16_t v; memcpy(&v, p, 2); return v;
}
static float rd_f32(const unsigned char *p) {
float v; memcpy(&v, p, 4); return v;
}
static float bf16_to_f32(uint16_t h) {
uint32_t f = (uint32_t)h << 16;
float v; memcpy(&v, &f, 4); return v;
}
static float fp16_to_f32(uint16_t h) {
uint32_t sign = (uint32_t)(h >> 15) & 1u;
uint32_t exp = (h >> 10) & 0x1fu;
uint32_t mant = h & 0x3ffu;
uint32_t bits;
float f;
if (exp == 0x1fu) { bits = (sign << 31) | 0x7f800000u | (mant << 13); memcpy(&f, &bits, 4); return f; }
if (exp == 0) {
if (mant == 0) { bits = sign << 31; memcpy(&f, &bits, 4); return f; }
f = (float)mant * 5.9604644775390625e-8f;
return sign ? -f : f;
}
bits = (sign << 31) | ((exp - 15 + 127) << 23) | (mant << 13);
memcpy(&f, &bits, 4);
return f;
}
/* 块大小 / 每块字节数(llama.cpp ggml 类型;Q4_0 等 block=32 */
static uint32_t type_block_bytes(uint32_t t) {
switch (t) {
case GGML_F32: return 32 * 4;
case GGML_F16: return 32 * 2;
case GGML_BF16: return 32 * 2;
case GGML_Q8_0: return 2 + 32; /* d(f16) + 32*int8 */
case GGML_Q4_0: return 2 + 16; /* d(f16) + 16*4bit(=32) */
case GGML_Q5_0: return 2 + 16 + 4; /* d + low + qh */
case GGML_Q5_1: return 2 + 2 + 16 + 4; /* d + m + low + qh */
default: return 0;
}
}
/* 反量化一个块(32 个)到 out */
static void dequant_block(uint32_t t, const unsigned char *p, float *out) {
int i;
switch (t) {
case GGML_F32: {
for (i = 0; i < 32; i++) out[i] = rd_f32(p + i * 4);
break;
}
case GGML_F16: {
for (i = 0; i < 32; i++) out[i] = fp16_to_f32(*(const uint16_t *)(p + i * 2));
break;
}
case GGML_BF16: {
for (i = 0; i < 32; i++) out[i] = bf16_to_f32(*(const uint16_t *)(p + i * 2));
break;
}
case GGML_Q8_0: {
float d = fp16_to_f32(*(const uint16_t *)p);
const int8_t *q = (const int8_t *)(p + 2);
for (i = 0; i < 32; i++) out[i] = (float)q[i] * d;
break;
}
case GGML_Q4_0: {
float d = fp16_to_f32(*(const uint16_t *)p);
const unsigned char *q = p + 2;
for (i = 0; i < 16; i++) {
int lo = q[i] & 0x0F;
int hi = (q[i] >> 4) & 0x0F;
out[i * 2 + 0] = (float)(lo - 8) * d;
out[i * 2 + 1] = (float)(hi - 8) * d;
}
break;
}
case GGML_Q5_0: {
float d = fp16_to_f32(*(const uint16_t *)p);
const unsigned char *ql = p + 2;
const unsigned char *qh = p + 2 + 16; /* 4 字节,bit w 对应权重 w 的高位 */
for (int w = 0; w < 32; w++) {
int low = (w & 1) ? (ql[w / 2] >> 4) & 0x0F : ql[w / 2] & 0x0F;
int high = (qh[w / 8] >> (w % 8)) & 1;
out[w] = (float)(low + (high << 4) - 16) * d;
}
break;
}
case GGML_Q5_1: {
float d = fp16_to_f32(*(const uint16_t *)p);
float m = fp16_to_f32(*(const uint16_t *)(p + 2));
const unsigned char *ql = p + 4;
const unsigned char *qh = p + 4 + 16;
for (int w = 0; w < 32; w++) {
int low = (w & 1) ? (ql[w / 2] >> 4) & 0x0F : ql[w / 2] & 0x0F;
int high = (qh[w / 8] >> (w % 8)) & 1;
out[w] = (float)(low + (high << 4)) * d + m;
}
break;
}
default: {
for (i = 0; i < 32; i++) out[i] = 0.0f;
break;
}
}
}
/* ---- 张量 ---- */
int gguf_has_tensor(const Gguf *g, const char *name) {
for (uint64_t i = 0; i < g->tensor_count; i++) {
if (strcmp(g->tensor_name[i], name) == 0) return 1;
}
return 0;
}
/* 计算张量元素个数 = prod(dims) */
static uint64_t tensor_n(const Gguf *g, uint64_t i) {
uint64_t n = 1;
for (uint32_t d = 0; d < g->n_dims[i]; d++) n *= g->dims[i][d];
return n;
}
long gguf_tensor_to_f32(const Gguf *g, const char *name, float *out) {
for (uint64_t i = 0; i < g->tensor_count; i++) {
if (strcmp(g->tensor_name[i], name) != 0) continue;
uint64_t n = tensor_n(g, i);
uint32_t t = g->ggml_type[i];
uint32_t bb = type_block_bytes(t);
if (bb == 0) return -1;
const unsigned char *p = gd(g, g->data_offset + (size_t)g->offset[i]);
uint64_t blocks = (n + 31) / 32;
uint64_t k = 0;
for (uint64_t b = 0; b < blocks; b++) {
float tmp[32];
dequant_block(t, p + (size_t)b * bb, tmp);
for (int j = 0; j < 32 && k < n; j++) out[k++] = tmp[j];
}
return (long)n;
}
return -1;
}
/* ---- 解析 ---- */
/* 版本自适应长度:GGUF v1 用 u32,v2/v3 用 u64 */
static size_t rd_len(int ver, const unsigned char *p, size_t *off) {
uint64_t v;
if (ver >= 2) { v = rd_u64(p + *off); *off += 8; }
else { v = rd_u32(p + *off); *off += 4; }
return (size_t)v;
}
/* 跳过 value,返回跳过的字节数。ver 用于数组/字符串长度宽度 */
static size_t skip_gval(int ver, const unsigned char *p, uint32_t vt) {
switch (vt) {
case GGVAL_UINT8: case GGVAL_INT8: case GGVAL_BOOL: return 1;
case GGVAL_UINT16: case GGVAL_INT16: return 2;
case GGVAL_UINT32: case GGVAL_INT32: case GGVAL_FLOAT32: return 4;
case GGVAL_FLOAT64: case GGVAL_UINT64: case GGVAL_INT64: return 8;
case GGVAL_STRING: { size_t o = 0; size_t n = rd_len(ver, p, &o); return o + n; }
case GGVAL_ARRAY: {
size_t o = 0;
uint32_t it = rd_u32(p + o); o += 4;
size_t n = rd_len(ver, p, &o);
const unsigned char *q = p + o;
for (size_t i = 0; i < n; i++) q += skip_gval(ver, q, it);
return (size_t)(q - p);
}
default: return 0;
}
}
static char *read_gstr(int ver, const unsigned char *p, size_t *off) {
size_t n = rd_len(ver, p, off);
char *s = malloc((size_t)n + 1);
memcpy(s, p + *off, (size_t)n);
s[n] = '\0';
*off += (size_t)n;
return s;
}
int gguf_open(Gguf *g, const char *path) {
memset(g, 0, sizeof(*g));
FILE *fh = fopen(path, "rb");
if (!fh) return -1;
fseek(fh, 0, SEEK_END);
long sz = ftell(fh);
fseek(fh, 0, SEEK_SET);
if (sz <= 0) { fclose(fh); return -2; }
unsigned char *buf = malloc((size_t)sz);
if (!buf) { fclose(fh); return -3; }
if (fread(buf, 1, (size_t)sz, fh) != (size_t)sz) { free(buf); fclose(fh); return -4; }
fclose(fh);
if (sz < 24) { free(buf); return -5; }
if (memcmp(buf, "GGUF", 4) != 0) { free(buf); return -6; }
g->data = buf;
g->size = (size_t)sz;
size_t off = 4;
g->version = rd_u32(buf + off); off += 4;
g->tensor_count = rd_len(g->version, buf, &off);
g->meta_count = rd_len(g->version, buf, &off);
/* 探测段顺序:GGUF 生产者可能写“元数据在前”或“张量在前”。
.weight */
int meta_first = 1;
{
size_t o = off;
size_t n0 = rd_len(g->version, buf, &o);
if (n0 >= 7 && memcmp(buf + o + n0 - 7, ".weight", 7) == 0) meta_first = 0;
}
/* 元数据 */
g->meta_key = calloc((size_t)g->meta_count, sizeof(char *));
g->meta_type = calloc((size_t)g->meta_count, sizeof(uint32_t));
g->meta_off = calloc((size_t)g->meta_count, sizeof(const unsigned char *));
g->meta_len = calloc((size_t)g->meta_count, sizeof(size_t));
/* 张量信息 */
g->tensor_name = calloc((size_t)g->tensor_count, sizeof(char *));
g->n_dims = calloc((size_t)g->tensor_count, sizeof(uint32_t));
g->dims = calloc((size_t)g->tensor_count, sizeof(uint64_t *));
g->ggml_type = calloc((size_t)g->tensor_count, sizeof(uint32_t));
g->offset = calloc((size_t)g->tensor_count, sizeof(uint64_t));
#define READ_METADATA() \
for (uint64_t i = 0; i < g->meta_count; i++) { \
g->meta_key[i] = read_gstr(g->version, buf, &off); \
uint32_t vt = rd_u32(buf + off); off += 4; \
g->meta_type[i] = vt; \
g->meta_off[i] = buf + off; \
size_t len = skip_gval(g->version, buf + off, vt); \
g->meta_len[i] = len; \
off += len; \
}
#define READ_TENSORS() \
for (uint64_t i = 0; i < g->tensor_count; i++) { \
g->tensor_name[i] = read_gstr(g->version, buf, &off); \
g->n_dims[i] = rd_u32(buf + off); off += 4; \
g->dims[i] = malloc((size_t)g->n_dims[i] * sizeof(uint64_t)); \
for (uint32_t d = 0; d < g->n_dims[i]; d++) { \
g->dims[i][d] = rd_len(g->version, buf, &off); \
} \
g->ggml_type[i] = rd_u32(buf + off); off += 4; \
g->offset[i] = rd_len(g->version, buf, &off); \
}
if (meta_first) { READ_METADATA(); READ_TENSORS(); }
else { READ_TENSORS(); READ_METADATA(); }
#undef READ_METADATA
#undef READ_TENSORS
g->data_offset = off;
return 0;
}
/* 查找元数据索引,未命中返回 -1 */
static ptrdiff_t meta_index(const Gguf *g, const char *key) {
for (uint64_t i = 0; i < g->meta_count; i++) {
if (strcmp(g->meta_key[i], key) == 0) return (ptrdiff_t)i;
}
return -1;
}
int gguf_meta_u32(const Gguf *g, const char *key, uint32_t *v) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
switch (g->meta_type[i]) {
case GGVAL_UINT8: *v = p[0]; return 1;
case GGVAL_UINT16: *v = rd_u16(p); return 1;
case GGVAL_UINT32: *v = rd_u32(p); return 1;
case GGVAL_INT32: *v = rd_u32(p); return 1;
case GGVAL_UINT64: *v = (uint32_t)rd_u64(p); return 1;
case GGVAL_INT64: *v = (uint32_t)rd_u64(p); return 1;
default: return 0;
}
}
int gguf_meta_f32(const Gguf *g, const char *key, float *v) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
if (g->meta_type[i] == GGVAL_FLOAT32) { *v = rd_f32(p); return 1; }
if (g->meta_type[i] == GGVAL_FLOAT64) {
union { double d; uint64_t b; } u; memcpy(&u.b, p, 8); *v = (float)u.d; return 1;
}
return 0;
}
int gguf_meta_u32_array(const Gguf *g, const char *key, uint32_t *out, uint64_t max) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
if (g->meta_type[i] != GGVAL_ARRAY) return 0;
uint32_t it = rd_u32(p);
size_t o = 4;
size_t n = rd_len(g->version, p, &o);
if (it != GGVAL_UINT32) return 0;
size_t take = n < max ? n : max;
for (size_t k = 0; k < take; k++) out[k] = rd_u32(p + o + k * 4);
return 1;
}
int gguf_meta_u8_array(const Gguf *g, const char *key, uint8_t *out, uint64_t max) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
if (g->meta_type[i] == GGVAL_ARRAY) {
uint32_t it = rd_u32(p);
size_t o = 4;
size_t n = rd_len(g->version, p, &o);
if (it != GGVAL_UINT8) return 0;
size_t take = n < max ? n : max;
for (size_t k = 0; k < take; k++) out[k] = p[o + k];
return 1;
}
if (g->meta_type[i] == GGVAL_STRING) {
size_t o = 0;
size_t n = rd_len(g->version, p, &o);
size_t take = n < max ? n : max;
for (size_t k = 0; k < take; k++) out[k] = p[o + k];
return 1;
}
return 0;
}
int gguf_meta_string(const Gguf *g, const char *key, char *out, size_t max) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
if (g->meta_type[i] == GGVAL_STRING) {
size_t o = 0;
size_t n = rd_len(g->version, p, &o);
size_t take = (size_t)n < max ? (size_t)n : max - 1;
memcpy(out, p + o, take);
out[take] = '\0';
return 1;
}
return 0;
}
int gguf_meta_string_array(const Gguf *g, const char *key, char ***out, int *count) {
ptrdiff_t i = meta_index(g, key);
if (i < 0) return 0;
const unsigned char *p = g->meta_off[i];
if (g->meta_type[i] != GGVAL_ARRAY) return 0;
uint32_t it = rd_u32(p);
size_t o = 4;
size_t n = rd_len(g->version, p, &o);
if (it != GGVAL_STRING) return 0;
char **arr = calloc((size_t)n, sizeof(char *));
const unsigned char *q = p + o;
for (size_t k = 0; k < n; k++) {
size_t so = 0;
size_t sl = rd_len(g->version, q, &so);
arr[k] = malloc((size_t)sl + 1);
memcpy(arr[k], q + so, (size_t)sl);
arr[k][sl] = '\0';
q += so + (size_t)sl;
}
if (out) *out = arr;
if (count) *count = (int)n;
return 1;
}
void gguf_free_string_array(char **arr, int count) {
if (!arr) return;
for (int i = 0; i < count; i++) free(arr[i]);
free(arr);
}
void gguf_close(Gguf *g) {
if (!g) return;
for (uint64_t i = 0; i < g->tensor_count; i++) {
free(g->tensor_name[i]);
free(g->dims[i]);
}
for (uint64_t i = 0; i < g->meta_count; i++) free(g->meta_key[i]);
free(g->tensor_name);
free(g->n_dims);
free(g->dims);
free(g->ggml_type);
free(g->offset);
free(g->meta_key);
free(g->meta_type);
free(g->meta_off);
free(g->meta_len);
free((void *)g->data);
memset(g, 0, sizeof(*g));
}
static const char *type_name(uint32_t t) {
switch (t) {
case GGML_F32: return "F32";
case GGML_F16: return "F16";
case GGML_BF16: return "BF16";
case GGML_Q8_0: return "Q8_0";
case GGML_Q4_0: return "Q4_0";
case GGML_Q5_0: return "Q5_0";
case GGML_Q5_1: return "Q5_1";
default: return "?";
}
}
void gguf_inspect(const Gguf *g, const char *path, FILE *out) {
fprintf(out, "==== GGUF 文件: %s ====\n", path);
fprintf(out, "version=%u tensors=%llu metadata=%llu data_offset=%zu\n",
g->version,
(unsigned long long)g->tensor_count,
(unsigned long long)g->meta_count,
g->data_offset);
fprintf(out, "--- 张量表 ---\n");
for (uint64_t i = 0; i < g->tensor_count; i++) {
uint64_t n = tensor_n(g, i);
fprintf(out, " %-32s dims=", g->tensor_name[i]);
for (uint32_t d = 0; d < g->n_dims[i]; d++) fprintf(out, "%llu%s",
(unsigned long long)g->dims[i][d], d + 1 < g->n_dims[i] ? "x" : "");
fprintf(out, " type=%s elems=%llu bytes~%llu\n",
type_name(g->ggml_type[i]), (unsigned long long)n,
(unsigned long long)(type_block_bytes(g->ggml_type[i]) * ((n + 31) / 32)));
}
}
+60
查看文件
@@ -0,0 +1,60 @@
#ifndef MO_GGUF_H
#define MO_GGUF_H
#include <stdint.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
/* GGUF (llama.cpp) 容器读取。v2/v3。 */
typedef struct {
const unsigned char *data;
size_t size;
uint32_t version;
uint64_t tensor_count;
uint64_t meta_count;
size_t data_offset; /* 张量数据段起点(绝对文件偏移) */
/* 张量表 */
char **tensor_name;
uint32_t *n_dims;
uint64_t **dims; /* [i][d] */
uint32_t *ggml_type;
uint64_t *offset; /* 相对 data_offset */
/* 元数据(供查询) */
char **meta_key;
uint32_t *meta_type;
const unsigned char **meta_off; /* value 起始(在 buffer 内) */
size_t *meta_len; /* value 字节数 */
} Gguf;
/* 打开并解析文件。成功返回 0,失败返回负。 */
int gguf_open(Gguf *g, const char *path);
void gguf_close(Gguf *g);
/* 元数据查询:返回 1 命中,0 未命中 */
int gguf_meta_u32(const Gguf *g, const char *key, uint32_t *v);
int gguf_meta_f32(const Gguf *g, const char *key, float *v);
int gguf_meta_u32_array(const Gguf *g, const char *key, uint32_t *out, uint64_t max);
int gguf_meta_u8_array(const Gguf *g, const char *key, uint8_t *out, uint64_t max);
int gguf_meta_string(const Gguf *g, const char *key, char *out, size_t max);
/* 读字符串数组(如 tokenizer.ggml.tokens/merges)。成功返回 1,*out 需调用 gguf_free_string_array 释放 */
int gguf_meta_string_array(const Gguf *g, const char *key, char ***out, int *count);
void gguf_free_string_array(char **arr, int count);
/* 打印元数据与张量表到 out(任意 GGUF 都能看)。 */
void gguf_inspect(const Gguf *g, const char *path, FILE *out);
/* 把名为 name 的张量反量化成 f32 写入 outout 长度需 rows*cols)。成功返回字节数,失败<0 */
/* 这里张量按 flat 元素看待,返回元素个数。 */
long gguf_tensor_to_f32(const Gguf *g, const char *name, float *out);
int gguf_has_tensor(const Gguf *g, const char *name);
#ifdef __cplusplus
}
#endif
#endif /* MO_GGUF_H */
+442
查看文件
@@ -0,0 +1,442 @@
#include "model/llama.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "infer/sample.h"
#include "model/gguf.h"
#include "utils/logger.h"
/* ---- 微型算子 ---- */
static float siluf(float x) { return x / (1.0f + expf(-x)); }
static void rmsnorm(const float *x, const float *w, float eps, int d, float *y) {
float sq = 0.0f;
for (int i = 0; i < d; i++) sq += x[i] * x[i];
float inv = 1.0f / sqrtf(sq / (float)d + eps);
for (int i = 0; i < d; i++) y[i] = x[i] * inv * w[i];
}
/* y[cols] = x[k] @ W[k,cols] (行主序 W=(k,cols) */
static void matvec(const float *x, const float *W, int k, int cols, float *y) {
for (int j = 0; j < cols; j++) {
float s = 0.0f;
for (int i = 0; i < k; i++) s += x[i] * W[i * cols + j];
y[j] = s;
}
}
static void silu_inplace(float *x, int n) { for (int i = 0; i < n; i++) x[i] = siluf(x[i]); }
/* RoPE (NeoX half-rotate) 作用于一个 head 向量(长度 dim),位置 pos */
static void rope_inplace(const LlamaModel *m, float *v, int pos) {
int half = m->c.head_dim / 2;
const float *c = m->cos_cache[pos];
const float *s = m->sin_cache[pos];
for (int i = 0; i < half; i++) {
float a = v[i], b = v[i + half];
v[i] = a * c[i] - b * s[i];
v[i + half] = b * c[i] + a * s[i];
}
}
void llama_embed(const LlamaModel *m, int token, float *x) {
const float *te = m->token_embd + (size_t)token * m->c.n_embd;
memcpy(x, te, (size_t)m->c.n_embd * sizeof(float));
}
/* ---- MoErouter softmax → top-k → 重归一化 → SwiGLU 专家加权和 ---- */
static void moe_ffn(const LlamaModel *m, int l, const float *x, float *out) {
int d = m->c.n_embd, ne = m->c.n_expert, used = m->c.n_expert_used, de = m->c.ffn_dim;
float *probs = malloc((size_t)ne * sizeof(float));
matvec(x, m->router[l], d, ne, probs);
float mx = probs[0];
for (int e = 1; e < ne; e++) if (probs[e] > mx) mx = probs[e];
float sum = 0.0f;
for (int e = 0; e < ne; e++) { probs[e] = expf(probs[e] - mx); sum += probs[e]; }
for (int e = 0; e < ne; e++) probs[e] /= sum;
int *idx = malloc((size_t)used * sizeof(int));
for (int t = 0; t < used; t++) {
int best = -1; float bp = -1.0f;
for (int e = 0; e < ne; e++) {
int taken = 0;
for (int q = 0; q < t; q++) if (idx[q] == e) { taken = 1; break; }
if (!taken && probs[e] > bp) { bp = probs[e]; best = e; }
}
idx[t] = best;
}
float wsum = 0.0f;
for (int t = 0; t < used; t++) wsum += probs[idx[t]];
float *g = malloc((size_t)de * sizeof(float));
float *up = malloc((size_t)de * sizeof(float));
float *eo = malloc((size_t)d * sizeof(float));
for (int i = 0; i < d; i++) out[i] = 0.0f;
for (int t = 0; t < used; t++) {
int e = idx[t];
float w = probs[e] / (wsum + 1e-9f);
matvec(x, m->ffn_gate[l][e], d, de, g); silu_inplace(g, de);
matvec(x, m->ffn_up[l][e], d, de, up);
for (int i = 0; i < de; i++) g[i] *= up[i];
matvec(g, m->ffn_down[l][e], de, d, eo);
for (int i = 0; i < d; i++) out[i] += w * eo[i];
}
free(probs); free(idx); free(g); free(up); free(eo);
}
/* ---- 稠密 SwiGLUdown(silu(gate(x)) * up(x)) ---- */
static void dense_ffn(const LlamaModel *m, int l, const float *x, float *out) {
int d = m->c.n_embd, de = m->c.ffn_dim;
float *g = malloc((size_t)de * sizeof(float));
float *up = malloc((size_t)de * sizeof(float));
matvec(x, m->df_gate[l], d, de, g); silu_inplace(g, de);
matvec(x, m->df_up[l], d, de, up);
for (int i = 0; i < de; i++) g[i] *= up[i];
matvec(g, m->df_down[l], de, d, out);
free(g); free(up);
}
/* ---- 单位置前向 ---- */
void llama_forward(LlamaModel *m, int pos, const float *x, float *logits) {
const LlamaConfig *c = &m->c;
int d = c->n_embd, H = c->n_head, HK = c->n_head_kv, hd = c->head_dim;
int hpk = H / HK; /* 每组 kv 对应查询头数 */
int L = c->n_layer, V = c->vocab;
float *a = malloc((size_t)d * sizeof(float));
float *q = malloc((size_t)(H * hd) * sizeof(float));
float *k = malloc((size_t)(HK * hd) * sizeof(float));
float *v = malloc((size_t)(HK * hd) * sizeof(float));
float *att= malloc((size_t)(H * hd) * sizeof(float));
float *o = malloc((size_t)d * sizeof(float));
float *ffn= malloc((size_t)d * sizeof(float));
float *scores = malloc((size_t)(m->c.max_seq) * sizeof(float));
float *h = malloc((size_t)d * sizeof(float));
memcpy(h, x, (size_t)d * sizeof(float));
float isq = 1.0f / sqrtf((float)hd);
for (int l = 0; l < L; l++) {
rmsnorm(h, m->attn_norm[l], c->rmsnorm_eps, d, a);
matvec(a, m->attn_q[l], d, H * hd, q);
matvec(a, m->attn_k[l], d, HK * hd, k);
matvec(a, m->attn_v[l], d, HK * hd, v);
for (int hh = 0; hh < H; hh++) rope_inplace(m, q + (size_t)hh * hd, pos);
for (int hh = 0; hh < HK; hh++) rope_inplace(m, k + (size_t)hh * hd, pos);
/* 存 KV cache */
memcpy(m->k_cache[l] + (size_t)pos * HK * hd, k, (size_t)HK * hd * sizeof(float));
memcpy(m->v_cache[l] + (size_t)pos * HK * hd, v, (size_t)HK * hd * sizeof(float));
for (int hh = 0; hh < H; hh++) {
int kvh = hh / hpk;
const float *qh = q + (size_t)hh * hd;
for (int j = 0; j <= pos; j++) {
const float *kj = m->k_cache[l] + ((size_t)j * HK + kvh) * hd;
float s = 0.0f;
for (int t = 0; t < hd; t++) s += qh[t] * kj[t];
scores[j] = s * isq;
}
float mxs = scores[0];
for (int j = 1; j <= pos; j++) if (scores[j] > mxs) mxs = scores[j];
float ss = 0.0f;
for (int j = 0; j <= pos; j++) { scores[j] = expf(scores[j] - mxs); ss += scores[j]; }
float is = 1.0f / ss;
for (int j = 0; j <= pos; j++) scores[j] *= is;
for (int t = 0; t < hd; t++) {
float acc = 0.0f;
for (int j = 0; j <= pos; j++) {
const float *vj = m->v_cache[l] + ((size_t)j * HK + kvh) * hd;
acc += scores[j] * vj[t];
}
att[(size_t)hh * hd + t] = acc;
}
}
matvec(att, m->attn_o[l], d, d, o);
for (int i = 0; i < d; i++) h[i] += o[i];
rmsnorm(h, m->ffn_norm[l], c->rmsnorm_eps, d, a);
if (c->is_moe) moe_ffn(m, l, a, ffn); else dense_ffn(m, l, a, ffn);
for (int i = 0; i < d; i++) h[i] += ffn[i];
}
rmsnorm(h, m->output_norm, c->rmsnorm_eps, d, a);
matvec(a, m->output, d, V, logits);
free(a); free(q); free(k); free(v); free(att); free(o); free(ffn); free(scores); free(h);
}
/* ---- 生成:自回归,输出 token id 到 out_ids(返回产出 token 数) ---- */
int llama_generate(LlamaModel *m, const int *input_ids, int n_input,
int max_new, float temperature, int top_k, int *out_ids) {
const int V = m->c.vocab, maxseq = m->c.max_seq;
float *logits = malloc((size_t)V * sizeof(float));
float *x = malloc((size_t)m->c.n_embd * sizeof(float));
int *ids = malloc((size_t)(maxseq + 8) * sizeof(int));
int n = n_input < maxseq ? n_input : maxseq;
for (int i = 0; i < n; i++) ids[i] = input_ids[i];
for (int i = 0; i < n; i++) { llama_embed(m, ids[i], x); llama_forward(m, i, x, logits); }
int pos = n, produced = 0;
for (int t = 0; t < max_new; t++) {
if (pos >= maxseq) break;
int next = sample_top_k(logits, V, temperature, top_k);
if (out_ids) out_ids[produced] = next;
llama_embed(m, next, x);
llama_forward(m, pos, x, logits);
pos++;
produced++;
}
free(logits); free(x); free(ids);
return produced;
}
/* ---- 名字精确匹配 ---- */
static int parse_blk(const char *name, int *i, const char **suffix) {
if (strncmp(name, "blk.", 4) != 0) return 0;
const char *p = name + 4;
if (*p < '0' || *p > '9') return 0;
*i = 0;
while (*p >= '0' && *p <= '9') { *i = *i * 10 + (*p - '0'); p++; }
if (*p != '.') return 0;
*suffix = p;
return 1;
}
static int parse_expert(const char *name, int *i, int *e, const char **suffix) {
static const char *pfx = ".ffn_experts.";
const char *s;
if (!parse_blk(name, i, &s)) return 0;
if (strncmp(s, pfx, strlen(pfx)) != 0) return 0;
const char *q = s + strlen(pfx);
if (*q < '0' || *q > '9') return 0;
*e = 0;
while (*q >= '0' && *q <= '9') { *e = *e * 10 + (*q - '0'); q++; }
if (*q != '.') return 0;
*suffix = q;
return 1;
}
/* 装载一个名为 name 的张量 buf(长度 n,浮点已反量化) */
static void load_w(LlamaModel *m, const char *name, const float *buf, uint64_t n) {
LlamaConfig *c = &m->c;
(void)n;
if (strcmp(name, "token_embd.weight") == 0) { memcpy(m->token_embd, buf, (size_t)c->vocab * c->n_embd * 4); return; }
if (strcmp(name, "output_norm.weight") == 0) { memcpy(m->output_norm, buf, (size_t)c->n_embd * 4); return; }
if (strcmp(name, "output.weight") == 0) { memcpy(m->output, buf, (size_t)c->n_embd * c->vocab * 4); return; }
int i, e;
const char *s;
if (parse_expert(name, &i, &e, &s)) {
if (strcmp(s, ".ffn_gate.weight") == 0) memcpy(m->ffn_gate[i][e], buf, (size_t)c->n_embd * c->ffn_dim * 4);
else if (strcmp(s, ".ffn_up.weight") == 0) memcpy(m->ffn_up[i][e], buf, (size_t)c->n_embd * c->ffn_dim * 4);
else if (strcmp(s, ".ffn_down.weight") == 0) memcpy(m->ffn_down[i][e], buf, (size_t)c->ffn_dim * c->n_embd * 4);
return;
}
if (parse_blk(name, &i, &s)) {
if (strcmp(s, ".attn_norm.weight") == 0) memcpy(m->attn_norm[i], buf, (size_t)c->n_embd * 4);
else if (strcmp(s, ".attn_q.weight") == 0) memcpy(m->attn_q[i], buf, (size_t)c->n_embd * c->n_head * c->head_dim * 4);
else if (strcmp(s, ".attn_k.weight") == 0) memcpy(m->attn_k[i], buf, (size_t)c->n_embd * c->n_head_kv * c->head_dim * 4);
else if (strcmp(s, ".attn_v.weight") == 0) memcpy(m->attn_v[i], buf, (size_t)c->n_embd * c->n_head_kv * c->head_dim * 4);
else if (strcmp(s, ".attn_o.weight") == 0) memcpy(m->attn_o[i], buf, (size_t)c->n_embd * c->n_embd * 4);
else if (strcmp(s, ".ffn_norm.weight") == 0) memcpy(m->ffn_norm[i], buf, (size_t)c->n_embd * 4);
else if (strcmp(s, ".ffn_gate_inp.weight") == 0) memcpy(m->router[i], buf, (size_t)c->n_embd * c->n_expert * 4);
else if (strcmp(s, ".ffn_gate.weight") == 0) memcpy(m->df_gate[i], buf, (size_t)c->n_embd * c->ffn_dim * 4);
else if (strcmp(s, ".ffn_up.weight") == 0) memcpy(m->df_up[i], buf, (size_t)c->n_embd * c->ffn_dim * 4);
else if (strcmp(s, ".ffn_down.weight") == 0) memcpy(m->df_down[i], buf, (size_t)c->ffn_dim * c->n_embd * 4);
return;
}
fprintf(stderr, "[warn] 忽略未知权重: %s\n", name);
}
/* ---- 加载 GGUF ---- */
static int get_u32(const Gguf *g, const char *k, int def, int *out) {
uint32_t v; if (gguf_meta_u32(g, k, &v)) { *out = (int)v; return 1; } *out = def; return 0;
}
static int get_f32(const Gguf *g, const char *k, float def, float *out) {
if (gguf_meta_f32(g, k, out)) { return 1; }
*out = def;
return 0;
}
int llama_load(LlamaModel *m, const char *path) {
memset(m, 0, sizeof(*m));
Gguf g;
if (gguf_open(&g, path) != 0) { fprintf(stderr, "[err] GGUF 打开失败: %s\n", path); return -1; }
LlamaConfig *c = &m->c;
get_u32(&g, "llama.block_count", 4, &c->n_layer);
get_u32(&g, "llama.embedding_length", 512, &c->n_embd);
get_u32(&g, "llama.attention.head_count", 8, &c->n_head);
get_u32(&g, "llama.attention.head_count_kv", c->n_head, &c->n_head_kv);
get_f32(&g, "llama.attention.layer_norm_rms_epsilon", 1e-5f, &c->rmsnorm_eps);
get_u32(&g, "llama.feed_forward_length", 1024, &c->ffn_dim);
get_u32(&g, "llama.expert_count", 0, &c->n_expert);
get_u32(&g, "llama.expert_used_count", 2, &c->n_expert_used);
get_u32(&g, "llama.context_length", 256, &c->max_seq);
get_u32(&g, "llama.rope.dimension_count", 0, &c->head_dim);
get_f32(&g, "llama.rope.freq_base", 10000.0f, &c->rope_theta);
get_u32(&g, "llama.vocab_size", 0, &c->vocab);
c->is_moe = c->n_expert > 0;
if (c->head_dim == 0) c->head_dim = c->n_embd / c->n_head;
/* vocab 从 token_embd 张量推断 */
if (c->vocab == 0 && gguf_has_tensor(&g, "token_embd.weight")) {
/* 读 dims: ne0 = n_embd, ne1 = vocab */
c->vocab = 0;
for (uint64_t i = 0; i < g.tensor_count; i++)
if (strcmp(g.tensor_name[i], "token_embd.weight") == 0) { c->vocab = (int)g.dims[i][1]; break; }
}
/* 分配权重 */
int L = c->n_layer, V = c->vocab, d = c->n_embd, E = c->n_expert, de = c->ffn_dim;
m->token_embd = malloc((size_t)V * d * 4);
m->output = malloc((size_t)d * V * 4);
m->output_norm = malloc((size_t)d * 4);
m->attn_norm = calloc(L, sizeof(float *));
m->attn_q = calloc(L, sizeof(float *));
m->attn_k = calloc(L, sizeof(float *));
m->attn_v = calloc(L, sizeof(float *));
m->attn_o = calloc(L, sizeof(float *));
m->ffn_norm = calloc(L, sizeof(float *));
m->router = calloc(L, sizeof(float *));
m->ffn_gate = calloc(L, sizeof(float **));
m->ffn_up = calloc(L, sizeof(float **));
m->ffn_down = calloc(L, sizeof(float **));
m->df_gate = calloc(L, sizeof(float *));
m->df_up = calloc(L, sizeof(float *));
m->df_down = calloc(L, sizeof(float *));
m->k_cache = calloc(L, sizeof(float *));
m->v_cache = calloc(L, sizeof(float *));
size_t kv_per = (size_t)c->max_seq * c->n_head_kv * c->head_dim;
for (int i = 0; i < L; i++) {
m->attn_norm[i] = malloc((size_t)d * 4);
m->attn_q[i] = malloc((size_t)d * c->n_head * c->head_dim * 4);
m->attn_k[i] = malloc((size_t)d * c->n_head_kv * c->head_dim * 4);
m->attn_v[i] = malloc((size_t)d * c->n_head_kv * c->head_dim * 4);
m->attn_o[i] = malloc((size_t)d * d * 4);
m->ffn_norm[i] = malloc((size_t)d * 4);
m->k_cache[i] = malloc(kv_per * 4);
m->v_cache[i] = malloc(kv_per * 4);
if (c->is_moe) {
m->router[i] = malloc((size_t)d * E * 4);
m->ffn_gate[i] = calloc((size_t)E, sizeof(float *));
m->ffn_up[i] = calloc((size_t)E, sizeof(float *));
m->ffn_down[i] = calloc((size_t)E, sizeof(float *));
for (int e = 0; e < E; e++) {
m->ffn_gate[i][e] = malloc((size_t)d * de * 4);
m->ffn_up[i][e] = malloc((size_t)d * de * 4);
m->ffn_down[i][e] = malloc((size_t)de * d * 4);
}
} else {
m->df_gate[i] = malloc((size_t)d * de * 4);
m->df_up[i] = malloc((size_t)d * de * 4);
m->df_down[i] = malloc((size_t)de * d * 4);
}
}
/* 位置编码缓存 cos/sin[max_seq][head_dim/2] */
int half = c->head_dim / 2;
m->cos_cache = malloc((size_t)c->max_seq * sizeof(float *));
m->sin_cache = malloc((size_t)c->max_seq * sizeof(float *));
for (int p = 0; p < c->max_seq; p++) {
m->cos_cache[p] = malloc((size_t)half * 4);
m->sin_cache[p] = malloc((size_t)half * 4);
for (int i = 0; i < half; i++) {
float theta = powf(c->rope_theta, -2.0f * i / (float)c->head_dim);
float ang = (float)p * theta;
m->cos_cache[p][i] = cosf(ang);
m->sin_cache[p][i] = sinf(ang);
}
}
/* 逐张量装载 */
for (uint64_t i = 0; i < g.tensor_count; i++) {
uint64_t n = 1;
for (uint32_t dd = 0; dd < g.n_dims[i]; dd++) n *= g.dims[i][dd];
float *tmp = malloc((size_t)n * 4);
if (gguf_tensor_to_f32(&g, g.tensor_name[i], tmp) > 0)
load_w(m, g.tensor_name[i], tmp, n);
free(tmp);
}
/* 分词器(tokenizer.ggml.tokens/merges */
{
char **toks = NULL; int ntok = 0;
if (gguf_meta_string_array(&g, "tokenizer.ggml.tokens", &toks, &ntok) && ntok > 0) {
char **mrg = NULL; int nmrg = 0;
gguf_meta_string_array(&g, "tokenizer.ggml.merges", &mrg, &nmrg);
llmtok_init(&m->tok, toks, ntok, mrg, nmrg);
gguf_free_string_array(toks, ntok);
gguf_free_string_array(mrg, nmrg);
}
}
gguf_close(&g);
m->is_loaded = 1;
return 0;
}
int llama_tokenize(const LlamaModel *m, const char *text, int *ids, int max) {
if (!m->is_loaded || m->tok.vocab_size == 0) return -1;
return llmtok_encode(&m->tok, text, ids, max);
}
int llama_detokenize(const LlamaModel *m, const int *ids, int n, char *out, int max) {
if (!m->is_loaded || m->tok.vocab_size == 0) return -1;
return llmtok_decode(&m->tok, ids, n, out, max);
}
void llama_free(LlamaModel *m) {
if (!m) return;
int L = m->c.n_layer, E = m->c.n_expert;
llmtok_free(&m->tok);
free(m->token_embd); free(m->output); free(m->output_norm);
for (int i = 0; i < L; i++) {
free(m->attn_norm[i]); free(m->attn_q[i]); free(m->attn_k[i]);
free(m->attn_v[i]); free(m->attn_o[i]); free(m->ffn_norm[i]);
free(m->k_cache[i]); free(m->v_cache[i]);
free(m->router[i]);
if (m->ffn_gate[i]) {
for (int e = 0; e < E; e++) { free(m->ffn_gate[i][e]); free(m->ffn_up[i][e]); free(m->ffn_down[i][e]); }
}
free(m->ffn_gate[i]); free(m->ffn_up[i]); free(m->ffn_down[i]);
free(m->df_gate[i]); free(m->df_up[i]); free(m->df_down[i]);
}
free(m->attn_norm); free(m->attn_q); free(m->attn_k); free(m->attn_v);
free(m->attn_o); free(m->ffn_norm); free(m->router);
free(m->ffn_gate); free(m->ffn_up); free(m->ffn_down);
free(m->df_gate); free(m->df_up); free(m->df_down);
free(m->k_cache); free(m->v_cache);
for (int p = 0; p < m->c.max_seq; p++) { free(m->cos_cache[p]); free(m->sin_cache[p]); }
free(m->cos_cache); free(m->sin_cache);
memset(m, 0, sizeof(*m));
}
int64_t llama_param_count(const LlamaModel *m) {
const LlamaConfig *c = &m->c;
int64_t n = 0;
n += (int64_t)c->vocab * c->n_embd; /* token_embd */
n += (int64_t)c->n_embd * c->vocab; /* output */
n += c->n_embd; /* output_norm */
for (int i = 0; i < c->n_layer; i++) {
n += c->n_embd; /* attn_norm */
n += (int64_t)c->n_embd * (c->n_head * c->head_dim); /* q */
n += (int64_t)c->n_embd * (c->n_head_kv * c->head_dim); /* k,v */
n += (int64_t)c->n_embd * (c->n_head_kv * c->head_dim);
n += (int64_t)c->n_embd * c->n_embd; /* o */
n += c->n_embd; /* ffn_norm */
if (c->is_moe) {
n += (int64_t)c->n_embd * c->n_expert; /* router */
n += 3LL * c->n_expert * c->n_embd * c->ffn_dim;
} else {
n += 3LL * c->n_embd * c->ffn_dim;
}
}
return n;
}
+65
查看文件
@@ -0,0 +1,65 @@
#ifndef MO_LLAMA_H
#define MO_LLAMA_H
#include <stddef.h>
#include <stdint.h>
#include "model/llama_tokenizer.h"
#ifdef __cplusplus
extern "C" {
#endif
/* llama / Mixtral 架构。所有权重以 (in, out) 行主序存储,C 端直接 x @ W。 */
typedef struct {
int vocab, n_layer, n_embd, n_head, n_head_kv, head_dim, ffn_dim;
int n_expert, n_expert_used, max_seq;
float rmsnorm_eps, rope_theta;
int is_moe; /* 1=Mixrtal MoE, 0=dense SwiGLU */
} LlamaConfig;
typedef struct {
LlamaConfig c;
float *token_embd; /* vocab * n_embd */
float *output_norm; /* n_embd */
float *output; /* n_embd * vocab */
float **attn_norm, **attn_q, **attn_k, **attn_v, **attn_o, **ffn_norm; /* [L] */
float **router; /* [L] n_embd * n_expert */
float ***ffn_gate, ***ffn_up, ***ffn_down; /* [L][E] */
float **df_gate, **df_up, **df_down; /* [L] 稠密路径 */
float **cos_cache, **sin_cache; /* [max_seq][head_dim/2] */
/* KV cache(内部使用) */
float **k_cache, **v_cache; /* [L] max_seq * n_head_kv * head_dim */
LlamaTokenizer tok; /* 分词器(若 GGUF 带 tokenizer.ggml.tokens */
int is_loaded;
} LlamaModel;
int llama_load(LlamaModel *m, const char *path);
void llama_free(LlamaModel *m);
int64_t llama_param_count(const LlamaModel *m);
/* 文本 <-> token id(若模型带分词器)。返回 token 数/字节数,负为错 */
int llama_tokenize(const LlamaModel *m, const char *text, int *ids, int max);
int llama_detokenize(const LlamaModel *m, const int *ids, int n, char *out, int max);
/* 输入嵌入:x = token_embd[token]RoPE 在 forward 内处理位置) */
void llama_embed(const LlamaModel *m, int token, float *x);
/* 对位置 pos 前向(会写入内部 KV cache),输出 logits(vocab) */
void llama_forward(LlamaModel *m, int pos, const float *x, float *logits);
/* 自回归生成:从 input_ids 开始,产出 token id 写入 out_ids(长度 max_new),返回产出数。 */
int llama_generate(LlamaModel *m, const int *input_ids, int n_input,
int max_new, float temperature, int top_k, int *out_ids);
#ifdef __cplusplus
}
#endif
#endif /* MO_LLAMA_H */
+168
查看文件
@@ -0,0 +1,168 @@
#define _GNU_SOURCE
#include "model/llama_tokenizer.h"
#include <stdlib.h>
#include <string.h>
/* ---- GPT-2 byte_to_unicode 映射 ---- */
/* b2u_str[b]byte b 映射成的 unicode 字符的 UTF-8 串;同时生成反向 code2byte[码点]=字节 */
static void build_b2u(char b2u[256][5], int code2byte[512]) {
int bs[256], nb = 0;
for (int i = '!'; i <= '~'; i++) bs[nb++] = i;
for (int i = 0xA1; i <= 0xAC; i++) bs[nb++] = i;
for (int i = 0xAE; i <= 0xFF; i++) bs[nb++] = i;
for (int i = 0; i < 512; i++) code2byte[i] = -1;
int byte2code[256];
for (int b = 0; b < 256; b++) byte2code[b] = -1;
for (int i = 0; i < nb; i++) byte2code[bs[i]] = bs[i]; /* 可打印区:字节映射到自身 */
int n = 0;
for (int b = 0; b < 256; b++) if (byte2code[b] < 0) byte2code[b] = 256 + n++;
for (int b = 0; b < 256; b++) {
int code = byte2code[b];
code2byte[code] = b;
if (code < 0x80) { b2u[b][0] = (char)code; b2u[b][1] = 0; }
else if (code < 0x800) {
b2u[b][0] = (char)(0xC0 | (code >> 6));
b2u[b][1] = (char)(0x80 | (code & 0x3F));
b2u[b][2] = 0;
} else {
b2u[b][0] = (char)(0xE0 | (code >> 12));
b2u[b][1] = (char)(0x80 | ((code >> 6) & 0x3F));
b2u[b][2] = (char)(0x80 | (code & 0x3F));
b2u[b][3] = 0;
}
}
}
/* 二分查找 token 字符串 */
static int lookup(const LlamaTokenizer *t, const char *s) {
int lo = 0, hi = t->vocab_size - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
int c = strcmp(t->tokens[t->sort_idx[mid]], s);
if (c == 0) return t->sort_idx[mid];
if (c < 0) lo = mid + 1; else hi = mid - 1;
}
return -1;
}
static int cmp_idx(const void *a, const void *b, void *arg) {
char **tokens = (char **)arg;
const int *ia = (const int *)a, *ib = (const int *)b;
return strcmp(tokens[*ia], tokens[*ib]);
}
int llmtok_init(LlamaTokenizer *t, char **tokens, int vocab_size, char **merges, int num_merges) {
memset(t, 0, sizeof(*t));
t->vocab_size = vocab_size;
t->tokens = calloc((size_t)vocab_size, sizeof(char *));
for (int i = 0; i < vocab_size; i++) {
size_t n = strlen(tokens[i]);
t->tokens[i] = malloc(n + 1);
memcpy(t->tokens[i], tokens[i], n + 1);
}
t->sort_idx = malloc((size_t)vocab_size * sizeof(int));
for (int i = 0; i < vocab_size; i++) t->sort_idx[i] = i;
qsort_r(t->sort_idx, (size_t)vocab_size, sizeof(int), cmp_idx, t->tokens);
/* byte -> id */
char b2u[256][5];
int code2byte[512];
build_b2u(b2u, code2byte);
t->byte_to_id = malloc(256 * sizeof(int));
for (int b = 0; b < 256; b++) t->byte_to_id[b] = lookup(t, b2u[b]);
/* merges "a b" -> id 对 */
t->num_merges = num_merges;
t->ma = malloc((size_t)(num_merges > 0 ? num_merges : 1) * sizeof(int));
t->mb = malloc((size_t)(num_merges > 0 ? num_merges : 1) * sizeof(int));
for (int i = 0; i < num_merges; i++) {
const char *s = merges[i];
const char *sp = strchr(s, ' ');
int la = sp ? (int)(sp - s) : (int)strlen(s);
char a[512], b[512];
memcpy(a, s, (size_t)la); a[la] = 0;
strcpy(b, sp ? sp + 1 : "");
t->ma[i] = lookup(t, a);
t->mb[i] = lookup(t, b);
}
return 0;
}
void llmtok_free(LlamaTokenizer *t) {
if (!t) return;
for (int i = 0; i < t->vocab_size; i++) free(t->tokens[i]);
free(t->tokens);
free(t->byte_to_id);
free(t->sort_idx);
free(t->ma);
free(t->mb);
memset(t, 0, sizeof(*t));
}
int llmtok_encode(const LlamaTokenizer *t, const char *text, int *out, int max) {
size_t n = strlen(text);
if (n == 0) return 0;
int *ids = malloc((size_t)(n + 8) * sizeof(int));
int len = 0;
for (size_t i = 0; i < n; i++) ids[len++] = t->byte_to_id[(unsigned char)text[i]];
while (1) {
int best = -1, bestr = 0x7fffffff;
for (int i = 0; i + 1 < len; i++) {
int a = ids[i], b = ids[i + 1];
for (int r = 0; r < t->num_merges; r++) {
if (t->ma[r] == a && t->mb[r] == b && r < bestr) { bestr = r; best = i; break; }
}
}
if (best < 0) break;
int a = ids[best], b = ids[best + 1];
int la = (int)strlen(t->tokens[a]), lb = (int)strlen(t->tokens[b]);
char buf[2048];
memcpy(buf, t->tokens[a], (size_t)la);
memcpy(buf + la, t->tokens[b], (size_t)lb + 1);
int merged = lookup(t, buf);
if (merged < 0) break;
ids[best] = merged;
memmove(&ids[best + 1], &ids[best + 2], (size_t)(len - best - 2) * sizeof(int));
len--;
}
if (len > max) { free(ids); return -1; }
for (int i = 0; i < len; i++) out[i] = ids[i];
free(ids);
return len;
}
/* 把一个 UTF-8 字符解码成码点 */
static int u8_to_cp(const unsigned char *s, int *len) {
unsigned char c = s[0];
if (c < 0x80) { *len = 1; return c; }
if ((c & 0xE0) == 0xC0) { *len = 2; return ((c & 0x1F) << 6) | (s[1] & 0x3F); }
if ((c & 0xF0) == 0xE0) { *len = 3; return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); }
*len = 1; return c;
}
int llmtok_decode(const LlamaTokenizer *t, const int *ids, int n, char *out, int maxcat) {
/* 反 byte_to_unicode:码点 -> 字节 */
char b2u[256][5];
int code2byte[512];
build_b2u(b2u, code2byte);
int w = 0;
for (int i = 0; i < n && w < maxcat; i++) {
int id = ids[i];
if (id < 0 || id >= t->vocab_size) continue;
const char *s = t->tokens[id];
while (*s && w < maxcat) {
int l; int cp = u8_to_cp((const unsigned char *)s, &l);
int b = (cp >= 0 && cp < 512) ? code2byte[cp] : (cp & 0xFF);
out[w++] = (char)b;
s += l;
}
}
if (w < maxcat) out[w] = '\0'; else out[maxcat - 1] = '\0';
return w;
}
+31
查看文件
@@ -0,0 +1,31 @@
#ifndef MO_LLAMA_TOKENIZER_H
#define MO_LLAMA_TOKENIZER_H
#ifdef __cplusplus
extern "C" {
#endif
/* GPT-2 / llama 的 byte-level BPE 分词器(读 GGUF tokenizer.ggml.tokens/merges)。 */
typedef struct {
int vocab_size;
char **tokens; /* 深拷贝,[vocab_size] */
int *byte_to_id; /* [256] 字节 -> token id */
int *sort_idx; /* [vocab_size] 按 tokens 字符串排序的索引(二分查找用) */
int num_merges;
int *ma, *mb; /* [num_merges] 被合并的两个 token id */
} LlamaTokenizer;
/* 用 tokens/merges 字符串构建(深拷贝 tokens/merges)。merges 为 "a b" 形式。成功返回 0。 */
int llmtok_init(LlamaTokenizer *t, char **tokens, int vocab_size, char **merges, int num_merges);
void llmtok_free(LlamaTokenizer *t);
/* 编码:text -> ids(最多 max 个)。返回 token 数,负为错。 */
int llmtok_encode(const LlamaTokenizer *t, const char *text, int *out, int max);
/* 解码:ids -> utf-8(最多 max 字节)。返回字节数。 */
int llmtok_decode(const LlamaTokenizer *t, const int *ids, int n, char *out, int max);
#ifdef __cplusplus
}
#endif
#endif /* MO_LLAMA_TOKENIZER_H */
+435
查看文件
@@ -0,0 +1,435 @@
#include "model/model.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "model/gguf.h"
#define PAP_MAGIC "PAP1"
static uint32_t rd_u32(const unsigned char *b, size_t *off) {
uint32_t v;
memcpy(&v, b + *off, 4);
*off += 4;
return v;
}
static float rd_f32(const unsigned char *b, size_t *off) {
float v;
memcpy(&v, b + *off, 4);
*off += 4;
return v;
}
static int expect_magic(const unsigned char *b, size_t *off) {
if (memcmp(b + *off, PAP_MAGIC, 4) != 0) return -1;
*off += 4;
return 0;
}
/* 精确匹配:sscanf 成功赋值且整个名字都被消费(避免尾部字面量未匹配却部分成功) */
static int m1(const char *name, const char *fmt, int *a) {
int n = 0;
int r = sscanf(name, fmt, a, &n);
return (r == 1) && name[n] == '\0';
}
static int m2(const char *name, const char *fmt, int *a, int *b) {
int n = 0;
int r = sscanf(name, fmt, a, b, &n);
return (r == 2) && name[n] == '\0';
}
/* 把第 layer 层第 expert 个 MoE 专家的 w1/w2 指针(按需分配)填入 m。 */
static void ensure_moe_expert(Model *m, int layer, int expert) {
if (!m->exp_w1[layer][expert]) {
m->exp_w1[layer][expert] = malloc((size_t)m->config.d_model * m->config.d_expert * sizeof(float));
m->exp_w2[layer][expert] = malloc((size_t)m->config.d_expert * m->config.d_model * sizeof(float));
}
}
static void load_weight(Model *m, const char *name, uint32_t rows, uint32_t cols, const float *src) {
(void)rows;
(void)cols;
ModelConfig *c = &m->config;
int l, e;
if (strcmp(name, "wte") == 0) {
memcpy(m->wte, src, (size_t)c->vocab_size * c->d_model * sizeof(float));
} else if (strcmp(name, "wpe") == 0) {
memcpy(m->wpe, src, (size_t)c->max_seq_len * c->d_model * sizeof(float));
} else if (strcmp(name, "norm_final") == 0) {
memcpy(m->norm_final, src, (size_t)c->d_model * sizeof(float));
} else if (strcmp(name, "lm_head") == 0) {
memcpy(m->lm_head, src, (size_t)c->vocab_size * c->d_model * sizeof(float));
} else if (m1(name, "l%d.norm1%n", &l)) {
memcpy(m->l_norm1[l], src, (size_t)c->d_model * sizeof(float));
} else if (m1(name, "l%d.attn.wq%n", &l)) {
memcpy(m->wq[l], src, (size_t)c->d_model * c->d_model * sizeof(float));
} else if (m1(name, "l%d.attn.wk%n", &l)) {
memcpy(m->wk[l], src, (size_t)c->d_model * c->d_model * sizeof(float));
} else if (m1(name, "l%d.attn.wv%n", &l)) {
memcpy(m->wv[l], src, (size_t)c->d_model * c->d_model * sizeof(float));
} else if (m1(name, "l%d.attn.wo%n", &l)) {
memcpy(m->wo[l], src, (size_t)c->d_model * c->d_model * sizeof(float));
} else if (m1(name, "l%d.norm2%n", &l)) {
memcpy(m->l_norm2[l], src, (size_t)c->d_model * sizeof(float));
} else if (m1(name, "l%d.moe.router%n", &l)) {
memcpy(m->router[l], src, (size_t)c->d_model * c->moe_n_experts * sizeof(float));
} else if (m2(name, "l%d.moe.exp%d.w1%n", &l, &e)) {
ensure_moe_expert(m, l, e);
memcpy(m->exp_w1[l][e], src, (size_t)c->d_model * c->d_expert * sizeof(float));
} else if (m2(name, "l%d.moe.exp%d.w2%n", &l, &e)) {
ensure_moe_expert(m, l, e);
memcpy(m->exp_w2[l][e], src, (size_t)c->d_expert * c->d_model * sizeof(float));
} else if (m1(name, "l%d.ffn.w1%n", &l)) {
memcpy(m->ffn_w1[l], src, (size_t)c->d_model * c->d_ff * sizeof(float));
} else if (m1(name, "l%d.ffn.w2%n", &l)) {
memcpy(m->ffn_w2[l], src, (size_t)c->d_ff * c->d_model * sizeof(float));
} else {
fprintf(stderr, "[warn] 忽略未知权重: %s\n", name);
}
}
/* fp16 -> fp32 */
static float half_to_float(uint16_t h) {
uint32_t sign = (uint32_t)(h >> 15) & 1u;
uint32_t exp = (h >> 10) & 0x1fu;
uint32_t mant = h & 0x3ffu;
uint32_t bits;
float f;
if (exp == 0x1fu) { /* inf / nan */
bits = (sign << 31) | 0x7f800000u | (mant << 13);
memcpy(&f, &bits, 4);
return f;
}
if (exp == 0) { /* 0 或次正规 */
if (mant == 0) { bits = sign << 31; memcpy(&f, &bits, 4); return f; }
f = (float)mant * 5.9604644775390625e-8f; /* mant * 2^-24 */
return sign ? -f : f;
}
bits = (sign << 31) | ((exp - 15 + 127) << 23) | (mant << 13);
memcpy(&f, &bits, 4);
return f;
}
/* 把原始权重字节按 dtype 反量化成 f32 写入 dst,返回读走的字节数 */
static size_t read_weight_f32(const unsigned char *p, uint32_t rows, uint32_t cols,
uint32_t dtype, float *dst) {
size_t n = (size_t)rows * cols;
if (dtype == 0) {
memcpy(dst, p, n * 4);
return n * 4;
}
if (dtype == 1) {
for (size_t i = 0; i < n; i++) {
uint16_t h;
memcpy(&h, p + i * 2, 2);
dst[i] = half_to_float(h);
}
return n * 2;
}
if (dtype == 2) { /* q8: 每块 [fp16 scale][32 int8] */
size_t k = 0, po = 0;
size_t nb = (n + 31) / 32;
for (size_t b = 0; b < nb; b++) {
uint16_t hs;
memcpy(&hs, p + po, 2);
po += 2;
float scale = half_to_float(hs);
size_t cnt = (n - k) < 32 ? (n - k) : 32;
for (size_t j = 0; j < cnt; j++) {
dst[k++] = (float)(int8_t)p[po + j] * (scale / 127.0f);
}
po += cnt;
}
return po;
}
return 0;
}
/* 根据已就绪的 config 分配全部权重存储(供 .pap/.gguf 加载器复用)。失败返回 -1 */
static int alloc_weights(Model *m) {
ModelConfig *c = &m->config;
int L = c->n_layer;
m->wte = malloc((size_t)c->vocab_size * c->d_model * sizeof(float));
m->wpe = malloc((size_t)c->max_seq_len * c->d_model * sizeof(float));
m->l_norm1 = calloc(L, sizeof(float *));
m->wq = calloc(L, sizeof(float *));
m->wk = calloc(L, sizeof(float *));
m->wv = calloc(L, sizeof(float *));
m->wo = calloc(L, sizeof(float *));
m->l_norm2 = calloc(L, sizeof(float *));
m->router = calloc(L, sizeof(float *));
m->exp_w1 = calloc(L, sizeof(float **));
m->exp_w2 = calloc(L, sizeof(float **));
m->ffn_w1 = calloc(L, sizeof(float *));
m->ffn_w2 = calloc(L, sizeof(float *));
m->norm_final = malloc((size_t)c->d_model * sizeof(float));
m->lm_head = malloc((size_t)c->vocab_size * c->d_model * sizeof(float));
for (int l = 0; l < L; l++) {
m->l_norm1[l] = malloc((size_t)c->d_model * sizeof(float));
m->wq[l] = malloc((size_t)c->d_model * c->d_model * sizeof(float));
m->wk[l] = malloc((size_t)c->d_model * c->d_model * sizeof(float));
m->wv[l] = malloc((size_t)c->d_model * c->d_model * sizeof(float));
m->wo[l] = malloc((size_t)c->d_model * c->d_model * sizeof(float));
m->l_norm2[l] = malloc((size_t)c->d_model * sizeof(float));
m->exp_w1[l] = calloc((size_t)c->moe_n_experts, sizeof(float *));
m->exp_w2[l] = calloc((size_t)c->moe_n_experts, sizeof(float *));
if (c->moe_mask[l]) {
m->router[l] = malloc((size_t)c->d_model * c->moe_n_experts * sizeof(float));
} else {
m->ffn_w1[l] = malloc((size_t)c->d_model * c->d_ff * sizeof(float));
m->ffn_w2[l] = malloc((size_t)c->d_ff * c->d_model * sizeof(float));
}
}
return 0;
}
int model_load(Model *m, const char *path) {
if (!m || !path) return -1;
memset(m, 0, sizeof(*m));
FILE *fh = fopen(path, "rb");
if (!fh) { perror("open"); return -2; }
fseek(fh, 0, SEEK_END);
long sz = ftell(fh);
fseek(fh, 0, SEEK_SET);
if (sz <= 0) { fclose(fh); return -3; }
unsigned char *buf = malloc((size_t)sz);
if (!buf) { fclose(fh); return -4; }
size_t nrd = fread(buf, 1, (size_t)sz, fh);
fclose(fh);
if (nrd != (size_t)sz) { free(buf); return -5; }
size_t off = 0;
ModelConfig *c = &m->config;
config_init(c);
if (expect_magic(buf, &off) != 0) { free(buf); return -10; }
uint32_t version = rd_u32(buf, &off);
if (version != 1) { free(buf); return -11; }
uint32_t dtype = rd_u32(buf, &off);
if (dtype > 2) { free(buf); return -12; } /* 0=f32,1=fp16,2=q8 */
c->vocab_size = (int)rd_u32(buf, &off);
c->n_layer = (int)rd_u32(buf, &off);
c->d_model = (int)rd_u32(buf, &off);
c->n_head = (int)rd_u32(buf, &off);
c->d_ff = (int)rd_u32(buf, &off);
c->moe_n_experts = (int)rd_u32(buf, &off);
c->moe_top_k = (int)rd_u32(buf, &off);
c->d_expert = (int)rd_u32(buf, &off);
c->max_seq_len = (int)rd_u32(buf, &off);
c->num_merges = (int)rd_u32(buf, &off);
c->n_special = (int)rd_u32(buf, &off);
c->tie_weights = buf[off]; off += 4; /* 1 byte + 3 reserve */
c->rmsnorm_eps = rd_f32(buf, &off);
c->head_dim = c->d_model / c->n_head;
/* moe_mask */
c->moe_mask = malloc((size_t)c->n_layer);
memcpy(c->moe_mask, buf + off, (size_t)c->n_layer);
off += (size_t)c->n_layer;
/* ---- vocab blob ---- */
uint32_t blob_len = rd_u32(buf, &off);
size_t boff = 0;
const unsigned char *blob = buf + off;
off += blob_len;
unsigned char **vbytes = calloc((size_t)c->vocab_size, sizeof(unsigned char *));
int *vlen = calloc((size_t)c->vocab_size, sizeof(int));
for (int i = 0; i < c->vocab_size; i++) {
uint32_t tl = rd_u32(blob, &boff);
vbytes[i] = (unsigned char *)(blob + boff); /* 指向 buffer 内 */
vlen[i] = (int)tl;
boff += tl;
}
uint32_t *ma = malloc((size_t)(c->num_merges > 0 ? c->num_merges : 1) * sizeof(uint32_t));
uint32_t *mb = malloc((size_t)(c->num_merges > 0 ? c->num_merges : 1) * sizeof(uint32_t));
for (int i = 0; i < c->num_merges; i++) {
ma[i] = rd_u32(blob, &boff);
mb[i] = rd_u32(blob, &boff);
}
tokenizer_build(&m->tok, c->vocab_size, (const unsigned char **)vbytes, vlen,
c->num_merges, ma, mb);
free(vbytes);
free(vlen);
free(ma);
free(mb);
/* ---- weights 段 ---- */
uint32_t n_tensors = rd_u32(buf, &off);
if (alloc_weights(m) != 0) { free(buf); return -14; }
for (uint32_t t = 0; t < n_tensors; t++) {
uint32_t nl = rd_u32(buf, &off);
char name[128];
if (nl >= sizeof(name)) nl = sizeof(name) - 1;
memcpy(name, buf + off, nl);
name[nl] = '\0';
off += nl;
uint32_t rows = rd_u32(buf, &off);
uint32_t cols = rd_u32(buf, &off);
size_t n = (size_t)rows * cols;
float *tmp = malloc(n * sizeof(float));
if (!tmp) { free(buf); return -13; }
size_t used = read_weight_f32(buf + off, rows, cols, dtype, tmp);
off += used;
load_weight(m, name, rows, cols, tmp);
free(tmp);
}
m->is_loaded = 1;
free(buf);
return 0;
}
/* 从 GGUF 加载。要求使用我们定义的元数据键(见 platform/gguf_format.py*/
int model_load_gguf(Model *m, const char *path) {
if (!m || !path) return -1;
memset(m, 0, sizeof(*m));
Gguf g;
if (gguf_open(&g, path) != 0) { fprintf(stderr, "[err] 无法解析 GGUF: %s\n", path); return -20; }
ModelConfig *c = &m->config;
config_init(c);
uint32_t u;
if (!gguf_meta_u32(&g, "pap.vocab_size", &u)) { gguf_close(&g); return -21; } c->vocab_size = (int)u;
if (!gguf_meta_u32(&g, "pap.n_layer", &u)) { gguf_close(&g); return -21; } c->n_layer = (int)u;
if (!gguf_meta_u32(&g, "pap.d_model", &u)) { gguf_close(&g); return -21; } c->d_model = (int)u;
if (!gguf_meta_u32(&g, "pap.n_head", &u)) { gguf_close(&g); return -21; } c->n_head = (int)u;
if (!gguf_meta_u32(&g, "pap.d_ff", &u)) { gguf_close(&g); return -21; } c->d_ff = (int)u;
if (!gguf_meta_u32(&g, "pap.moe_n_experts", &u)) { gguf_close(&g); return -21; } c->moe_n_experts = (int)u;
if (!gguf_meta_u32(&g, "pap.moe_top_k", &u)) { gguf_close(&g); return -21; } c->moe_top_k = (int)u;
if (!gguf_meta_u32(&g, "pap.d_expert", &u)) { gguf_close(&g); return -21; } c->d_expert = (int)u;
if (!gguf_meta_u32(&g, "pap.max_seq_len", &u)) { gguf_close(&g); return -21; } c->max_seq_len = (int)u;
if (!gguf_meta_u32(&g, "pap.num_merges", &u)) { gguf_close(&g); return -21; } c->num_merges = (int)u;
gguf_meta_f32(&g, "pap.rmsnorm_eps", &c->rmsnorm_eps);
c->head_dim = c->d_model / c->n_head;
/* moe_mask */
if (c->n_layer > 0) {
c->moe_mask = malloc((size_t)c->n_layer);
uint64_t got = (uint64_t)c->n_layer;
if (!gguf_meta_u8_array(&g, "pap.moe_mask", c->moe_mask, got)) {
/* 缺省:全部 MoE */
for (int l = 0; l < c->n_layer; l++) c->moe_mask[l] = (c->moe_n_experts > 0) ? 1 : 0;
}
}
if (alloc_weights(m) != 0) { gguf_close(&g); return -22; }
/* 逐张量反量化并按名装载(未知名字会因精确匹配被忽略) */
for (uint64_t i = 0; i < g.tensor_count; i++) {
uint64_t n = 1;
for (uint32_t d = 0; d < g.n_dims[i]; d++) n *= g.dims[i][d];
float *tmp = malloc((size_t)n * sizeof(float));
if (gguf_tensor_to_f32(&g, g.tensor_name[i], tmp) > 0) {
load_weight(m, g.tensor_name[i], 1, (uint32_t)n, tmp);
}
free(tmp);
}
/* 词表 + merges(从元数据还原) */
if (c->vocab_size > 0) {
uint32_t *vlen = calloc((size_t)c->vocab_size, sizeof(uint32_t));
uint64_t blob_len = 0;
if (gguf_meta_u32_array(&g, "pap.vocab_len", vlen, (uint64_t)c->vocab_size)) {
for (int i = 0; i < c->vocab_size; i++) blob_len += vlen[i];
uint8_t *blob = malloc((size_t)blob_len);
if (gguf_meta_u8_array(&g, "pap.vocab_blob", blob, blob_len)) {
unsigned char **vbytes = calloc((size_t)c->vocab_size, sizeof(unsigned char *));
int *bytes = calloc((size_t)c->vocab_size, sizeof(int));
size_t off = 0;
for (int i = 0; i < c->vocab_size; i++) {
vbytes[i] = blob + off;
bytes[i] = (int)vlen[i];
off += vlen[i];
}
uint32_t *ma = calloc((size_t)(c->num_merges > 0 ? c->num_merges : 1), sizeof(uint32_t));
uint32_t *mb = calloc((size_t)(c->num_merges > 0 ? c->num_merges : 1), sizeof(uint32_t));
if (c->num_merges > 0) {
gguf_meta_u32_array(&g, "pap.merges_a", ma, (uint64_t)c->num_merges);
gguf_meta_u32_array(&g, "pap.merges_b", mb, (uint64_t)c->num_merges);
}
tokenizer_build(&m->tok, c->vocab_size, (const unsigned char **)vbytes, bytes,
c->num_merges, ma, mb);
free(vbytes); free(bytes); free(ma); free(mb);
}
free(blob);
}
free(vlen);
}
gguf_close(&g);
m->is_loaded = 1;
return 0;
}
void model_free(Model *m) {
if (!m) return;
int L = m->config.n_layer;
tokenizer_free(&m->tok);
free(m->wte);
free(m->wpe);
for (int l = 0; l < L; l++) {
free(m->l_norm1[l]);
free(m->wq[l]);
free(m->wk[l]);
free(m->wv[l]);
free(m->wo[l]);
free(m->l_norm2[l]);
free(m->router[l]);
if (m->exp_w1[l]) {
for (int e = 0; e < m->config.moe_n_experts; e++) {
free(m->exp_w1[l][e]);
free(m->exp_w2[l][e]);
}
}
free(m->exp_w1[l]);
free(m->exp_w2[l]);
free(m->ffn_w1[l]);
free(m->ffn_w2[l]);
}
free(m->l_norm1);
free(m->wq);
free(m->wk);
free(m->wv);
free(m->wo);
free(m->l_norm2);
free(m->router);
free(m->exp_w1);
free(m->exp_w2);
free(m->ffn_w1);
free(m->ffn_w2);
free(m->norm_final);
free(m->lm_head);
config_free(&m->config);
memset(m, 0, sizeof(*m));
}
int64_t model_param_count(const Model *m) {
const ModelConfig *c = &m->config;
int64_t n = 0;
n += (int64_t)c->vocab_size * c->d_model; /* wte */
n += (int64_t)c->max_seq_len * c->d_model; /* wpe */
for (int l = 0; l < c->n_layer; l++) {
n += c->d_model; /* norm1 */
n += 4LL * c->d_model * c->d_model; /* qkv+o */
n += c->d_model; /* norm2 */
if (c->moe_mask[l]) {
n += (int64_t)c->d_model * c->moe_n_experts; /* router */
n += 2LL * c->moe_n_experts * c->d_model * c->d_expert;
} else {
n += 2LL * c->d_model * c->d_ff;
}
}
n += c->d_model; /* norm_final */
n += (int64_t)c->vocab_size * c->d_model; /* lm_head */
return n;
}
+50
查看文件
@@ -0,0 +1,50 @@
#ifndef MO_MODEL_H
#define MO_MODEL_H
#include <stdint.h>
#include "model/config.h"
#include "model/tokenizer.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 已加载的模型:config + tokenizer + 全部权重(float32,行主序,列为 (in, out))。 */
typedef struct {
ModelConfig config;
Tokenizer tok;
/* 嵌入 */
float *wte; /* vocab * d_model */
float *wpe; /* max_seq * d_model */
/* 每层 */
float **l_norm1; /* [layer] d_model */
float **wq, **wk, **wv, **wo; /* [layer] d_model*d_model */
float **l_norm2; /* [layer] d_model */
float **router; /* [layer] d_model*n_experts;稠密层为 NULL */
float ***exp_w1; /* [layer][expert] d_model*d_expert */
float ***exp_w2; /* [layer][expert] d_expert*d_model */
float **ffn_w1; /* [layer] d_model*d_ff;MoE 层为 NULL */
float **ffn_w2; /* [layer] d_ff*d_model */
/* 最终 */
float *norm_final; /* d_model */
float *lm_head; /* vocab * d_model */
int is_loaded;
} Model;
/* 从 .pap 文件加载模型。成功返回 0 */
int model_load(Model *m, const char *path);
/* 从 GGUF 文件加载模型(使用我们定义的元数据键)。成功返回 0 */
int model_load_gguf(Model *m, const char *path);
void model_free(Model *m);
int64_t model_param_count(const Model *m);
#ifdef __cplusplus
}
#endif
#endif /* MO_MODEL_H */
+109
查看文件
@@ -0,0 +1,109 @@
#include "model/tokenizer.h"
#include <stdlib.h>
#include <string.h>
int tokenizer_build(Tokenizer *t,
int vocab_size, const unsigned char **vocab, const int *vocab_len,
int num_merges, const uint32_t *merge_a, const uint32_t *merge_b) {
if (!t || vocab_size <= 0) return -1;
t->vocab_size = vocab_size;
t->num_merges = num_merges;
t->vocab = calloc((size_t)vocab_size, sizeof(unsigned char *));
t->vocab_len = calloc((size_t)vocab_size, sizeof(int));
if (!t->vocab || !t->vocab_len) return -1;
for (int i = 0; i < vocab_size; i++) {
t->vocab[i] = malloc((size_t)(vocab_len[i] ? vocab_len[i] : 1));
if (!t->vocab[i]) return -1;
memcpy(t->vocab[i], vocab[i], (size_t)vocab_len[i]);
t->vocab_len[i] = vocab_len[i];
}
if (num_merges > 0) {
t->merge_a = malloc((size_t)num_merges * sizeof(uint32_t));
t->merge_b = malloc((size_t)num_merges * sizeof(uint32_t));
if (!t->merge_a || !t->merge_b) return -1;
memcpy(t->merge_a, merge_a, (size_t)num_merges * sizeof(uint32_t));
memcpy(t->merge_b, merge_b, (size_t)num_merges * sizeof(uint32_t));
} else {
t->merge_a = NULL;
t->merge_b = NULL;
}
return 0;
}
void tokenizer_free(Tokenizer *t) {
if (!t) return;
if (t->vocab) {
for (int i = 0; i < t->vocab_size; i++) free(t->vocab[i]);
free(t->vocab);
}
free(t->vocab_len);
free(t->merge_a);
free(t->merge_b);
memset(t, 0, sizeof(*t));
}
/* 找到 id 下标 [st, ed] 的字节拼接长度,用于 rank 遍历时比对。这里直接比较 merge 的 (a,b) 与当前 token 的 (id[i],id[i+1])。 */
static int merge_rank(const Tokenizer *t, uint32_t a, uint32_t b) {
for (int i = 0; i < t->num_merges; i++) {
if (t->merge_a[i] == a && t->merge_b[i] == b) return i;
}
return -1;
}
int tokenizer_encode(const Tokenizer *t, const char *text, int32_t *out_ids, int max_out) {
if (!t || !text || !out_ids) return -1;
size_t n = strlen(text);
if (n == 0) return 0;
/* 每个字节一个初始 token,id=字节值(0..255) */
int *tok = malloc((size_t)(n + 1) * sizeof(int));
if (!tok) return -1;
int len = 0;
for (size_t i = 0; i < n; i++) {
tok[len++] = (unsigned char)text[i];
}
const int base = 256;
while (1) {
int best = -1, best_rank = 0x7fffffff;
for (int i = 0; i + 1 < len; i++) {
int r = merge_rank(t, (uint32_t)tok[i], (uint32_t)tok[i + 1]);
if (r >= 0 && r < best_rank) {
best_rank = r;
best = i;
}
}
if (best < 0) break;
tok[best] = base + best_rank;
memmove(&tok[best + 1], &tok[best + 2], (size_t)(len - best - 2) * sizeof(int));
len--;
}
if (len > max_out) {
free(tok);
return -2;
}
for (int i = 0; i < len; i++) out_ids[i] = tok[i];
free(tok);
return len;
}
int tokenizer_decode(const Tokenizer *t, const int32_t *ids, int n, char *out, int max_out) {
if (!t || !ids || !out) return -1;
int w = 0;
for (int i = 0; i < n && w < max_out; i++) {
int id = ids[i];
if (id < 0 || id >= t->vocab_size || !t->vocab[id]) continue;
int L = t->vocab_len[id];
for (int j = 0; j < L && w < max_out; j++) {
/* 简单替换非法 UTF-8 连续字节为 '?' 的占位处理:原样拷贝,由读取端决定 */
out[w++] = (char)t->vocab[id][j];
}
}
if (w < max_out) out[w] = '\0';
else out[max_out - 1] = '\0';
return w;
}
+43
查看文件
@@ -0,0 +1,43 @@
#ifndef MO_TOKENIZER_H
#define MO_TOKENIZER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* byte-level BPE tokenizer platform/tokenizer.py
* id 0..255 = bytes([b])
* id 256+i = i merge token
* merges[i] = (a,b) -> id = 256 + i
* tokenn_special=0 BPE
*/
typedef struct {
int vocab_size;
int num_merges;
unsigned char **vocab; /* vocab_size 个字符串 */
int *vocab_len;
uint32_t *merge_a; /* num_merges */
uint32_t *merge_b;
} Tokenizer;
/* 用外部数据深拷贝构建(vocab/vocab_len/merge_a/merge_b 会被复制走) */
int tokenizer_build(Tokenizer *t,
int vocab_size, const unsigned char **vocab, const int *vocab_len,
int num_merges, const uint32_t *merge_a, const uint32_t *merge_b);
void tokenizer_free(Tokenizer *t);
/* 编码 text -> ids(最多 max_out 个)。返回 token 数,负数为错 */
int tokenizer_encode(const Tokenizer *t, const char *text, int32_t *out_ids, int max_out);
/* 解码 ids -> utf-8 文本(最多 max_out 字节)。返回写入字节数 */
int tokenizer_decode(const Tokenizer *t, const int32_t *ids, int n, char *out, int max_out);
#ifdef __cplusplus
}
#endif
#endif /* MO_TOKENIZER_H */
+222
查看文件
@@ -0,0 +1,222 @@
#include "server/api.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <jansson.h>
#include <microhttpd.h>
#include "infer/generate.h"
#include "utils/logger.h"
/* 简单监控计数(单线程 demo 用) */
static unsigned long g_requests = 0;
static unsigned long g_tokens = 0;
/* ---- 请求上下文 ---- */
typedef struct {
char *body;
size_t len;
} ConnInfo;
/* ---- JSON 字符串转义(写入 dst ---- */
static size_t json_escape(const char *src, char *dst, size_t cap) {
size_t w = 0;
for (const unsigned char *p = (const unsigned char *)src; *p && w + 8 < cap; p++) {
unsigned char c = *p;
if (c == '"') { dst[w++] = '\\'; dst[w++] = '"'; }
else if (c == '\\') { dst[w++] = '\\'; dst[w++] = '\\'; }
else if (c == '\n') { dst[w++] = '\\'; dst[w++] = 'n'; }
else if (c == '\r') { dst[w++] = '\\'; dst[w++] = 'r'; }
else if (c == '\t') { dst[w++] = '\\'; dst[w++] = 't'; }
else if (c < 0x20) { dst[w++] = ' '; }
else { dst[w++] = (char)c; }
}
dst[w] = '\0';
return w;
}
int moe_parse_generate_request(const char *json, char *prompt, size_t prompt_cap,
int *max_new, float *temperature, int *top_k) {
json_error_t err;
json_t *root = json_loads(json, 0, &err);
if (!root || !json_is_object(root)) {
if (root) json_decref(root);
return -1;
}
int rc = 0;
json_t *p = json_object_get(root, "prompt");
json_t *opts = json_object_get(root, "options");
if (!p || !json_is_string(p)) { rc = -2; goto done; }
const char *s = json_string_value(p);
size_t n = strlen(s);
if (n >= prompt_cap) n = prompt_cap - 1;
memcpy(prompt, s, n);
prompt[n] = '\0';
if (opts && json_is_object(opts)) {
json_t *mt = json_object_get(opts, "max_tokens");
if (mt && json_is_integer(mt)) *max_new = (int)json_integer_value(mt);
json_t *tmp = json_object_get(opts, "temperature");
if (tmp && json_is_real(tmp)) *temperature = (float)json_real_value(tmp);
json_t *tk = json_object_get(opts, "top_k");
if (tk && json_is_integer(tk)) *top_k = (int)json_integer_value(tk);
}
done:
json_decref(root);
return rc;
}
/* ---- SSE 回调:逐个 token 拉取并写入 SSE 行(返回写入字节数 / 结束标记) ---- */
static ssize_t sse_cb(void *cls, uint64_t pos, char *buf, size_t max) {
(void)pos;
GenStream *gs = (GenStream *)cls;
size_t w = 0;
char tok[64];
char esc[256];
while (w + 96 < max) {
int r = gen_stream_next(gs, tok, sizeof(tok));
if (r <= 0) {
/* 先冲掉已缓冲数据;否则结束流 */
return (ssize_t)(w > 0 ? w : MHD_CONTENT_READER_END_OF_STREAM);
}
g_tokens++; /* 监控:累计生成 token */
json_escape(tok, esc, sizeof(esc));
int n = snprintf(buf + w, max - w, "data: {\"response\":\"%s\"}\n\n", esc);
w += (size_t)n;
}
return (ssize_t)w;
}
static void sse_free(void *cls) {
GenStream *gs = (GenStream *)cls;
if (gs) {
gen_stream_end(gs);
free(gs);
}
}
/* ---- 连接完成时释放 ConnInfo ---- */
static void completion_cb(void *cls, struct MHD_Connection *conn, void **con_cls,
enum MHD_RequestTerminationCode code) {
(void)cls; (void)conn; (void)code;
ConnInfo *ci = *con_cls;
if (ci) {
free(ci->body);
free(ci);
*con_cls = NULL;
}
}
static char *read_body(ConnInfo *ci, const char *upload, size_t *upload_size) {
if (*upload_size == 0) return NULL;
ci->body = realloc(ci->body, ci->len + *upload_size + 1);
memcpy(ci->body + ci->len, upload, *upload_size);
ci->len += *upload_size;
ci->body[ci->len] = '\0';
size_t n = *upload_size;
*upload_size = 0;
(void)n;
return ci->body;
}
static enum MHD_Result send_text(struct MHD_Connection *conn, unsigned status, const char *ct, const char *body) {
struct MHD_Response *resp = MHD_create_response_from_buffer(strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
if (!resp) return MHD_NO;
MHD_add_response_header(resp, "Content-Type", ct);
enum MHD_Result ret = MHD_queue_response(conn, status, resp);
MHD_destroy_response(resp);
return ret;
}
static enum MHD_Result access_handler(void *cls, struct MHD_Connection *conn,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls) {
(void)version;
const Model *m = (const Model *)cls;
ConnInfo *ci = (ConnInfo *)*con_cls;
if (!ci) {
ci = calloc(1, sizeof(ConnInfo));
*con_cls = ci;
return MHD_YES;
}
/* 读取 POST 请求体 */
if (strcmp(method, MHD_HTTP_METHOD_POST) == 0) {
if (*upload_data_size) {
read_body(ci, upload_data, upload_data_size);
return MHD_YES;
}
}
/* GET /health */
if (strcmp(method, MHD_HTTP_METHOD_GET) == 0 && strcmp(url, "/health") == 0)
return send_text(conn, MHD_HTTP_OK, "text/plain", "ok");
/* GET /api/models */
if (strcmp(method, MHD_HTTP_METHOD_GET) == 0 && strcmp(url, "/api/models") == 0) {
char buf[256];
snprintf(buf, sizeof(buf),
"{\"model\":\"moe-0.1b\",\"params\":%lld,\"arch\":\"moe-pap\",\"n_experts\":%d}",
(long long)model_param_count(m), m->config.moe_n_experts);
return send_text(conn, MHD_HTTP_OK, "application/json", buf);
}
/* GET /api/metrics */
if (strcmp(method, MHD_HTTP_METHOD_GET) == 0 && strcmp(url, "/api/metrics") == 0) {
char buf[128];
snprintf(buf, sizeof(buf), "{\"requests\":%lu,\"tokens\":%lu}", g_requests, g_tokens);
return send_text(conn, MHD_HTTP_OK, "application/json", buf);
}
/* POST /api/generate (SSE) */
if (strcmp(method, MHD_HTTP_METHOD_POST) == 0 && strcmp(url, "/api/generate") == 0) {
g_requests++;
if (!ci->body) return send_text(conn, MHD_HTTP_BAD_REQUEST, "text/plain", "empty body");
char prompt[2048];
int max_new = 100, top_k = 40;
float temperature = 0.8f;
int rc = moe_parse_generate_request(ci->body, prompt, sizeof(prompt),
&max_new, &temperature, &top_k);
if (rc != 0) return send_text(conn, MHD_HTTP_BAD_REQUEST, "text/plain", "bad json");
GenStream *gs = calloc(1, sizeof(GenStream));
if (gen_stream_begin(gs, m, prompt, max_new, temperature, top_k) < 0) {
free(gs);
return send_text(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "text/plain", "gen init fail");
}
struct MHD_Response *resp = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN, 4096, &sse_cb, gs, &sse_free);
if (!resp) { gen_stream_end(gs); free(gs); return MHD_NO; }
MHD_add_response_header(resp, "Content-Type", "text/event-stream");
MHD_add_response_header(resp, "Cache-Control", "no-cache");
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
return send_text(conn, MHD_HTTP_NOT_FOUND, "text/plain", "not found");
}
int moe_server_run(const Model *m, const char *model_name, unsigned port) {
(void)model_name;
struct MHD_Daemon *daemon = MHD_start_daemon(
MHD_USE_THREAD_PER_CONNECTION,
(unsigned short)port, NULL, NULL, &access_handler, (void *)m,
MHD_OPTION_NOTIFY_COMPLETED, &completion_cb, NULL,
MHD_OPTION_END);
if (!daemon) {
MO_LOGE("HTTP 服务启动失败: %u", port);
return 1;
}
MO_LOGI("pmai HTTP 已启动: http://127.0.0.1:%u 模型=%s", port, model_name);
MO_LOGI(" GET /health");
MO_LOGI(" GET /api/models");
MO_LOGI(" GET /api/metrics");
MO_LOGI(" POST /api/generate (SSE) Ctrl-C 退出");
for (;;) sleep(3600); /* 阻塞直到 Ctrl-C 终止进程 */
return 0;
}
+24
查看文件
@@ -0,0 +1,24 @@
#ifndef MO_API_H
#define MO_API_H
#include <stddef.h>
#include "model/model.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 启动 HTTP 服务并阻塞。返回 0 正常退出,非0为失败。 */
int moe_server_run(const Model *m, const char *model_name, unsigned port);
/* 供测试:解析 JSON 请求体为 (prompt, max_new, temperature, top_k)。 */
/* 返回 0 成功,非0 失败。 */
int moe_parse_generate_request(const char *json, char *prompt, size_t prompt_cap,
int *max_new, float *temperature, int *top_k);
#ifdef __cplusplus
}
#endif
#endif /* MO_API_H */
+46
查看文件
@@ -0,0 +1,46 @@
#include "utils/logger.h"
#include <stdarg.h>
#include <stdio.h>
#include <time.h>
static MoLogLevel g_level = MO_LOG_INFO;
static int g_timestamps = 1;
static const char *level_name(MoLogLevel l) {
switch (l) {
case MO_LOG_DEBUG: return "DEBUG";
case MO_LOG_INFO: return "INFO";
case MO_LOG_WARN: return "WARN";
case MO_LOG_ERROR: return "ERROR";
default: return "????";
}
}
void mo_log_set_level(MoLogLevel level) { g_level = level; }
MoLogLevel mo_log_get_level(void) { return g_level; }
void mo_log_set_timestamps(int enabled) { g_timestamps = enabled; }
void mo_log_msg(MoLogLevel level, const char *fmt, ...) {
if (level < g_level) return;
FILE *out = (level >= MO_LOG_WARN) ? stderr : stdout;
if (g_timestamps) {
time_t t = time(NULL);
struct tm tm_buf;
char ts[32];
localtime_r(&t, &tm_buf); /* POSIX;Linux/glibc 下可用 */
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_buf);
fprintf(out, "%s [%5s] ", ts, level_name(level));
} else {
fprintf(out, "[%5s] ", level_name(level));
}
va_list args;
va_start(args, fmt);
vfprintf(out, fmt, args);
va_end(args);
fputc('\n', out);
fflush(out);
}
+40
查看文件
@@ -0,0 +1,40 @@
#ifndef MO_LOGGER_H
#define MO_LOGGER_H
#ifdef __cplusplus
extern "C" {
#endif
/* 日志级别 */
typedef enum {
MO_LOG_DEBUG = 0,
MO_LOG_INFO,
MO_LOG_WARN,
MO_LOG_ERROR,
MO_LOG_NONE
} MoLogLevel;
/* 控制全局日志级别 */
void mo_log_set_level(MoLogLevel level);
MoLogLevel mo_log_get_level(void);
/* 是否打印时间戳(默认开) */
void mo_log_set_timestamps(int enabled);
/* 一条日志;内部追加换行。level 低于全局级别时忽略 */
void mo_log_msg(MoLogLevel level, const char *fmt, ...)
#if defined(__GNUC__)
__attribute__((format(printf, 2, 3)))
#endif
;
#define MO_LOGD(...) mo_log_msg(MO_LOG_DEBUG, __VA_ARGS__)
#define MO_LOGI(...) mo_log_msg(MO_LOG_INFO, __VA_ARGS__)
#define MO_LOGW(...) mo_log_msg(MO_LOG_WARN, __VA_ARGS__)
#define MO_LOGE(...) mo_log_msg(MO_LOG_ERROR, __VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif /* MO_LOGGER_H */
+36
查看文件
@@ -0,0 +1,36 @@
#include "utils/math.h"
#include <math.h>
#include <stdint.h>
float mo_tanhf(float x) { return tanhf(x); }
float mo_sigmoidf(float x) {
/* 数值稳定:避免 expf 溢出 */
if (x >= 0.0f) {
float e = expf(-x);
return 1.0f / (1.0f + e);
} else {
float e = expf(x);
return e / (1.0f + e);
}
}
float mo_gelu(float x) {
/* GELU 的 tanh 近似:0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 x^3))) */
const float c = 0.7978845608028654f; /* sqrt(2/pi) */
const float a = 0.044715f;
return 0.5f * x * (1.0f + tanhf(c * (x + a * x * x * x)));
}
float mo_fast_exp(float x) {
/* Schraudolph 近似;Range 约为 (-88, 88),略偏大,适合做 softmax 相对比较 */
union { float f; int32_t i; } v;
v.i = (int32_t)(12102203.0f * x + 1065353216.0f);
return v.f;
}
float mo_fast_sigmoid(float x) {
float e = mo_fast_exp(-x);
return 1.0f / (1.0f + e);
}
+31
查看文件
@@ -0,0 +1,31 @@
#ifndef MO_MATH_H
#define MO_MATH_H
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 神经网络常用激活函数(标准库实现,精度优先) ---- */
/* tanh */
float mo_tanhf(float x);
/* sigmoid = 1 / (1 + exp(-x)) */
float mo_sigmoidf(float x);
/* GELUGPT-2 的 tanh 近似) */
float mo_gelu(float x);
/* ---- 快速近似(速度优先,精度有损) ---- */
/* 快速指数近似(Schraudolph 位技巧),用于需要高吞吐的路径 */
float mo_fast_exp(float x);
/* 快速 sigmoid,基于 mo_fast_exp */
float mo_fast_sigmoid(float x);
#ifdef __cplusplus
}
#endif
#endif /* MO_MATH_H */
+84
查看文件
@@ -0,0 +1,84 @@
#include "utils/mempool.h"
#include <stdlib.h>
#define MO_DEFAULT_BLOCK_SIZE (64u * 1024u)
#define MO_ALIGN 64u
static size_t align_up(size_t n, size_t a) {
return (n + a - 1u) & ~(a - 1u);
}
MoMemPool *mempool_create(size_t block_size) {
MoMemPool *p = calloc(1, sizeof(MoMemPool));
if (!p) return NULL;
p->block_size = block_size ? block_size : MO_DEFAULT_BLOCK_SIZE;
return p;
}
static MoMemPoolBlock *new_block(size_t min_cap, size_t block_size) {
/* 块容量向上取整到对齐边界;基址用 posix_memalign 保证强对齐,
MO_ALIGN SIMD/AVX */
size_t cap = align_up(min_cap > block_size ? min_cap : block_size, MO_ALIGN);
MoMemPoolBlock *b = calloc(1, sizeof(MoMemPoolBlock));
if (!b) return NULL;
int rc = posix_memalign((void **)&b->base, MO_ALIGN, cap);
if (rc != 0) {
free(b);
return NULL;
}
b->capacity = cap;
b->used = 0;
return b;
}
void *mempool_alloc(MoMemPool *pool, size_t size) {
if (!pool || size == 0) return NULL;
size = align_up(size, MO_ALIGN); /* 至少 64 对齐 */
/* 优先复用已有块 */
for (MoMemPoolBlock *b = pool->blocks; b; b = b->next) {
if (b->capacity - b->used >= size) {
void *p = b->base + b->used;
b->used += size;
return p;
}
}
/* 没有合适块,新建一个并头插 */
MoMemPoolBlock *b = new_block(size, pool->block_size);
if (!b) return NULL;
b->next = pool->blocks;
pool->blocks = b;
void *p = b->base + b->used;
b->used += size;
return p;
}
void mempool_clear(MoMemPool *pool) {
if (!pool) return;
for (MoMemPoolBlock *b = pool->blocks; b; b = b->next) {
b->used = 0;
}
}
void mempool_destroy(MoMemPool *pool) {
if (!pool) return;
MoMemPoolBlock *b = pool->blocks;
while (b) {
MoMemPoolBlock *next = b->next;
free(b->base);
free(b);
b = next;
}
free(pool);
}
size_t mempool_used_bytes(const MoMemPool *pool) {
size_t total = 0;
for (const MoMemPoolBlock *b = pool->blocks; b; b = b->next) {
total += b->used;
}
return total;
}
+51
查看文件
@@ -0,0 +1,51 @@
#ifndef MO_MEMPOOL_H
#define MO_MEMPOOL_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Arena
*
*
* malloc/free mempool_clear/destroy
*
* malloc free使
*/
typedef struct MoMemPoolBlock {
uint8_t *base; /* 块起始 */
size_t capacity; /* 块容量(字节) */
size_t used; /* 已用(字节,对齐后) */
struct MoMemPoolBlock *next;
} MoMemPoolBlock;
typedef struct {
size_t block_size; /* 每个新块的默认容量 */
MoMemPoolBlock *blocks;
} MoMemPool;
/* 创建内存池;block_size=0 时用默认 64KB。返回池指针,失败返回 NULL */
MoMemPool *mempool_create(size_t block_size);
/* 分配 size 字节(至少 64 字节对齐)。失败返回 NULL */
void *mempool_alloc(MoMemPool *pool, size_t size);
/* 清空所有已用空间(复用底层块,不释放) */
void mempool_clear(MoMemPool *pool);
/* 释放整个池及其所有块 */
void mempool_destroy(MoMemPool *pool);
/* 当前已用字节数 */
size_t mempool_used_bytes(const MoMemPool *pool);
#ifdef __cplusplus
}
#endif
#endif /* MO_MEMPOOL_H */
+131
查看文件
@@ -0,0 +1,131 @@
#include "core/matrix.h"
#include "core/tensor.h"
#include "utils/math.h"
#include "utils/mempool.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int g_pass = 0;
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { g_pass++; printf(" ok: %s\n", msg); } \
else { g_fail++; printf(" FAIL: %s\n", msg); } \
} while (0)
#define CHECK_NEAR(got, want, eps, msg) do { \
float _g = (float)(got), _w = (float)(want); \
if (fabsf(_g - _w) <= (eps)) { g_pass++; printf(" ok: %s (%.6f)\n", msg, _g); } \
else { g_fail++; printf(" FAIL: %s got %.6f want %.6f\n", msg, _g, _w); } \
} while (0)
static void test_tensor(void) {
printf("[tensor]\n");
int64_t shape[2] = {3, 4};
MoTensor *t = tensor_create(2, shape);
CHECK(t != NULL, "create 3x4");
CHECK(tensor_numel(t) == 12, "numel==12");
CHECK(t->strides[0] == 4 && t->strides[1] == 1, "row-major strides");
tensor_fill(t, 5.0f);
CHECK(t->data[0] == 5.0f && t->data[11] == 5.0f, "filled all");
int64_t idx[2] = {2, 3};
CHECK(tensor_offset(t, idx) == 11, "offset (2,3)==11");
int64_t flat[1] = {12};
MoTensor *r = tensor_reshape(t, 1, flat);
CHECK(r != NULL && r->ndim == 1, "reshape to 1D ok");
int64_t bad[1] = {13};
CHECK(tensor_reshape(t, 1, bad) == NULL, "reshape wrong size -> NULL");
MoTensor *clone = tensor_clone(t);
CHECK(clone != NULL && clone->data[5] == 5.0f, "clone preserves data");
tensor_free(clone);
tensor_free(t);
}
static void test_matmul(void) {
printf("[matmul]\n");
MoMat *a = mat_alloc(2, 3);
MoMat *b = mat_alloc(3, 2);
MoMat *c = mat_alloc(2, 2);
float ad[6] = {1, 2, 3, 4, 5, 6};
float bd[6] = {7, 8, 9, 10, 11, 12};
memcpy(a->data, ad, sizeof(ad));
memcpy(b->data, bd, sizeof(bd));
int rc = mat_mul(a, b, c);
CHECK(rc == 0, "mul success");
CHECK_NEAR(c->data[0], 58.0f, 1e-5f, "c[0][0]==58");
CHECK_NEAR(c->data[1], 64.0f, 1e-5f, "c[0][1]==64");
CHECK_NEAR(c->data[c->stride + 0], 139.0f, 1e-5f, "c[1][0]==139");
CHECK_NEAR(c->data[c->stride + 1], 154.0f, 1e-5f, "c[1][1]==154");
MoMat *w = mat_alloc(2, 2); /* wrong N */
CHECK(mat_mul(a, w, w) != 0, "mismatched dims -> error");
mat_free(w);
mat_free(a);
mat_free(b);
mat_free(c);
}
static void test_softmax(void) {
printf("[softmax]\n");
MoMat *m = mat_alloc(1, 5);
float d[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
memcpy(m->data, d, sizeof(d));
mat_softmax_rows(m);
float sum = 0.0f;
for (int j = 0; j < 5; j++) sum += m->data[j];
CHECK_NEAR(sum, 1.0f, 1e-5f, "row sums to 1");
CHECK(m->data[4] > m->data[0], "monotonic increasing");
mat_free(m);
}
static void test_math(void) {
printf("[math]\n");
CHECK_NEAR(mo_sigmoidf(0.0f), 0.5f, 1e-4f, "sigmoid(0)==0.5");
CHECK_NEAR(mo_tanhf(0.0f), 0.0f, 1e-5f, "tanh(0)==0");
CHECK(mo_gelu(0.0f) == 0.0f, "gelu(0)==0");
CHECK_NEAR(mo_gelu(1.0f), 0.841192f, 1e-3f, "gelu(1) approx");
CHECK(mo_fast_exp(1.0f) > 2.0f && mo_fast_exp(1.0f) < 3.0f, "fast_exp(1) in (2,3)");
}
static void test_mempool(void) {
printf("[mempool]\n");
MoMemPool *pool = mempool_create(0);
CHECK(pool != NULL, "create");
void *p1 = mempool_alloc(pool, 100);
void *p2 = mempool_alloc(pool, 5000);
CHECK(p1 != NULL && p2 != NULL, "two allocs");
CHECK(((uintptr_t)p1 & 63u) == 0, "64-byte aligned");
CHECK(p2 > p1, "distinct allocation");
/* 数据可写读回 */
((uint8_t *)p1)[0] = 0xAB;
((uint8_t *)p1)[99] = 0xCD;
CHECK(((uint8_t *)p1)[0] == 0xAB && ((uint8_t *)p1)[99] == 0xCD, "read/write intact");
mempool_clear(pool);
void *p3 = mempool_alloc(pool, 100);
CHECK(p3 != NULL, "alloc after clear");
mempool_destroy(pool);
CHECK(1, "destroy");
}
int main(void) {
printf("=== moe-serve core tests ===\n");
test_tensor();
test_matmul();
test_softmax();
test_math();
test_mempool();
printf("=== result: %d passed, %d failed ===\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
}
+42
查看文件
@@ -0,0 +1,42 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "model/gguf.h"
#include "model/llama_tokenizer.h"
/* 独立验证 C 端 GPT-2 byte-level BPE 分词器(不依赖完整模型加载/架构)。 */
int main(int argc, char **argv) {
if (argc < 2) { fprintf(stderr, "用法: test_llmtok <model.gguf> [text...]\n"); return 1; }
/* 从第 2 个参数起拼接成测试文本(若无则用默认)。 */
char text[4096] = "Hello, world! Today is a good day.";
if (argc > 2) { text[0] = 0; for (int i = 2; i < argc; i++) { strcat(text, argv[i]); if (i + 1 < argc) strcat(text, " "); } }
Gguf g;
if (gguf_open(&g, argv[1]) != 0) { fprintf(stderr, "gguf open fail\n"); return 2; }
char **toks = NULL; int ntok = 0;
if (!gguf_meta_string_array(&g, "tokenizer.ggml.tokens", &toks, &ntok)) { fprintf(stderr, "no tokens meta\n"); return 3; }
char **mrg = NULL; int nmrg = 0;
gguf_meta_string_array(&g, "tokenizer.ggml.merges", &mrg, &nmrg);
fprintf(stderr, "[info] vocab=%d merges=%d\n", ntok, nmrg);
LlamaTokenizer t;
if (llmtok_init(&t, toks, ntok, mrg, nmrg) != 0) { fprintf(stderr, "tok init fail\n"); return 4; }
gguf_free_string_array(toks, ntok);
gguf_free_string_array(mrg, nmrg);
int ids[512];
int n = llmtok_encode(&t, text, ids, 512);
printf("text: %s\n", text);
printf("ids: ");
for (int i = 0; i < n; i++) printf("%d ", ids[i]);
printf("\n");
char buf[4096];
int b = llmtok_decode(&t, ids, n, buf, sizeof(buf));
printf("round-trip: %.*s\n", b < 0 ? 0 : b, buf);
llmtok_free(&t);
gguf_close(&g);
return 0;
}