edgejev 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edgejev-0.1.0/LICENSE +15 -0
- edgejev-0.1.0/PKG-INFO +231 -0
- edgejev-0.1.0/README.md +190 -0
- edgejev-0.1.0/edgejev/__init__.py +10 -0
- edgejev-0.1.0/edgejev/agent.py +101 -0
- edgejev-0.1.0/edgejev/backends/__init__.py +20 -0
- edgejev-0.1.0/edgejev/backends/base.py +24 -0
- edgejev-0.1.0/edgejev/backends/laya.py +108 -0
- edgejev-0.1.0/edgejev/backends/playjev.py +111 -0
- edgejev-0.1.0/edgejev/build.py +275 -0
- edgejev-0.1.0/edgejev/cli.py +63 -0
- edgejev-0.1.0/edgejev/evaluate.py +126 -0
- edgejev-0.1.0/edgejev/post.py +73 -0
- edgejev-0.1.0/edgejev/providers.py +38 -0
- edgejev-0.1.0/edgejev/runtimes/__init__.py +1 -0
- edgejev-0.1.0/edgejev/runtimes/torch_vlm.py +84 -0
- edgejev-0.1.0/edgejev/serve.py +75 -0
- edgejev-0.1.0/edgejev/tokenize.py +17 -0
- edgejev-0.1.0/edgejev/train/__init__.py +15 -0
- edgejev-0.1.0/edgejev/train/data.py +83 -0
- edgejev-0.1.0/edgejev/train/losses.py +68 -0
- edgejev-0.1.0/edgejev/train/model.py +63 -0
- edgejev-0.1.0/edgejev.egg-info/PKG-INFO +231 -0
- edgejev-0.1.0/edgejev.egg-info/SOURCES.txt +28 -0
- edgejev-0.1.0/edgejev.egg-info/dependency_links.txt +1 -0
- edgejev-0.1.0/edgejev.egg-info/entry_points.txt +2 -0
- edgejev-0.1.0/edgejev.egg-info/requires.txt +24 -0
- edgejev-0.1.0/edgejev.egg-info/top_level.txt +1 -0
- edgejev-0.1.0/pyproject.toml +45 -0
- edgejev-0.1.0/setup.cfg +4 -0
edgejev-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
edgejev-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edgejev
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 在自己的设备上跑 Jev 式的类型化决策:ONNX + INT8,运行时不需要 torch,支持 Linux / macOS / Windows
|
|
5
|
+
Author-email: yzfly <zphyix@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/yzfly/edgejev
|
|
8
|
+
Project-URL: Repository, https://github.com/yzfly/edgejev
|
|
9
|
+
Project-URL: Issues, https://github.com/yzfly/edgejev/issues
|
|
10
|
+
Keywords: jev,system-one,onnx,quantization,cpu-inference,calibrated-probabilities
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: onnxruntime>=1.17
|
|
20
|
+
Requires-Dist: tokenizers>=0.15
|
|
21
|
+
Requires-Dist: numpy>=1.20
|
|
22
|
+
Requires-Dist: huggingface_hub>=0.20
|
|
23
|
+
Provides-Extra: build
|
|
24
|
+
Requires-Dist: torch>=2.0; extra == "build"
|
|
25
|
+
Requires-Dist: transformers>=4.45; extra == "build"
|
|
26
|
+
Requires-Dist: safetensors>=0.4; extra == "build"
|
|
27
|
+
Requires-Dist: laya>=0.3.0; extra == "build"
|
|
28
|
+
Requires-Dist: onnxconverter-common>=1.14; extra == "build"
|
|
29
|
+
Requires-Dist: onnx>=1.15; extra == "build"
|
|
30
|
+
Provides-Extra: vlm
|
|
31
|
+
Requires-Dist: torch>=2.0; extra == "vlm"
|
|
32
|
+
Requires-Dist: torchvision; extra == "vlm"
|
|
33
|
+
Requires-Dist: transformers>=4.45; extra == "vlm"
|
|
34
|
+
Requires-Dist: pillow; extra == "vlm"
|
|
35
|
+
Requires-Dist: accelerate; extra == "vlm"
|
|
36
|
+
Provides-Extra: train
|
|
37
|
+
Requires-Dist: torch>=2.0; extra == "train"
|
|
38
|
+
Requires-Dist: transformers>=4.45; extra == "train"
|
|
39
|
+
Requires-Dist: safetensors>=0.4; extra == "train"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# EdgeJev
|
|
43
|
+
|
|
44
|
+
在本地设备上跑 [Jev](https://typesafe.ai) 式的类型化决策。ONNX + INT8,运行时不需要 torch。
|
|
45
|
+
|
|
46
|
+
Jev 不生成文本:给一份 state 和几个带类型的问题,一次前向答完,返回能直接 `if` 的值加一个校准概率。
|
|
47
|
+
EdgeJev 把各家开源复现统一成「转换 → 量化 → 部署」一条路径。
|
|
48
|
+
|
|
49
|
+
4 vCPU Xeon 上 `laya-multilingual`(322M)单题 **15.6 ms**,模型 324 MB。
|
|
50
|
+
|
|
51
|
+
## 安装
|
|
52
|
+
|
|
53
|
+
还没发到 PyPI,从 GitHub 装:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# uv(推荐,自己管虚拟环境)
|
|
57
|
+
uv tool install "git+https://github.com/yzfly/edgejev"
|
|
58
|
+
|
|
59
|
+
# 或装进项目的虚拟环境
|
|
60
|
+
uv venv && uv pip install "git+https://github.com/yzfly/edgejev"
|
|
61
|
+
|
|
62
|
+
# pipx
|
|
63
|
+
pipx install "git+https://github.com/yzfly/edgejev"
|
|
64
|
+
|
|
65
|
+
# 原生 venv(Debian/Ubuntu 直接 pip 会报 externally-managed-environment)
|
|
66
|
+
python3 -m venv .venv && .venv/bin/pip install "git+https://github.com/yzfly/edgejev"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
可选依赖按需加,`uv pip install "edgejev[build] @ git+https://github.com/yzfly/edgejev"`:
|
|
70
|
+
|
|
71
|
+
| extra | 用途 |
|
|
72
|
+
| :-- | :-- |
|
|
73
|
+
| `build` | `edgejev build` 转 ONNX,需要 torch / transformers / laya |
|
|
74
|
+
| `vlm` | `playjev` 后端,需要 torch / torchvision / transformers / pillow |
|
|
75
|
+
| `train` | `edgejev.train` 训练模块 |
|
|
76
|
+
|
|
77
|
+
不加 extra 时运行时只有 onnxruntime + tokenizers + numpy。
|
|
78
|
+
|
|
79
|
+
## 用法
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
edgejev build --backend laya --out ./jev-int8
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from edgejev import Agent
|
|
87
|
+
|
|
88
|
+
ag = Agent("./jev-int8")
|
|
89
|
+
r = ag.system_one("我的信用卡被扣了两次款,麻烦退一笔。", {
|
|
90
|
+
"dept": {"type": "choice", "instructions": "该转给哪个组?",
|
|
91
|
+
"criteria": {"billing": "支付、扣款、发票、退款",
|
|
92
|
+
"technical": "程序缺陷、报错",
|
|
93
|
+
"sales": "售前咨询、定价"}},
|
|
94
|
+
"urgent": {"type": "noul", "instructions": "这条消息表达了紧急或时间压力"},
|
|
95
|
+
"anger": {"type": "score", "instructions": "客户的不满程度",
|
|
96
|
+
"criteria": ["平静陈述", "有情绪但讲道理", "非常愤怒"]},
|
|
97
|
+
})
|
|
98
|
+
r["answers"]["dept"]["choice"] # 'billing'
|
|
99
|
+
r["answers"]["dept"]["probabilities"] # {'billing': 0.92, 'technical': 0.08, 'sales': 0.00}
|
|
100
|
+
r["answers"]["anger"]["confidence"] # 0.41
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
官方协议端点:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
edgejev serve --model ./jev-int8 --port 8009
|
|
107
|
+
export TYPESAFE_BASE_URL=http://127.0.0.1:8009
|
|
108
|
+
export TYPESAFE_API_KEY=local
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 命令
|
|
112
|
+
|
|
113
|
+
| 命令 | 作用 |
|
|
114
|
+
| :-- | :-- |
|
|
115
|
+
| `edgejev build` | checkpoint → ONNX → 量化 → 多形状自检 |
|
|
116
|
+
| `edgejev serve` | `POST /v1/systemone` |
|
|
117
|
+
| `edgejev eval` | AG News / dair-ai emotion 上跑指标 |
|
|
118
|
+
| `edgejev bench` | 测延迟 |
|
|
119
|
+
| `edgejev info` | provider 与已注册后端 |
|
|
120
|
+
|
|
121
|
+
## 后端
|
|
122
|
+
|
|
123
|
+
| 后端 | 骨干 | 读出方式 | 运行时 | 状态 |
|
|
124
|
+
| :-- | :-- | :-- | :-- | :-- |
|
|
125
|
+
| `laya` | mmBERT-base 322M | 每题一行,读 `[MASK]` 标记位 | ONNX | ✅ |
|
|
126
|
+
| `playjev` | Qwen3.5-0.8B VLM | 画面输入,读最后位置的字母槽 logits | torch | ✅ |
|
|
127
|
+
| `kev` | Qwen + LoRA 0.5B–8B | 多题打包,block-causal,PointerHead | ONNX | 🚧 |
|
|
128
|
+
|
|
129
|
+
`playjev` 走 torch:Qwen3.5 的文本塔是混合线性注意力(`layer_types` 含 `linear_attention`),
|
|
130
|
+
依赖 `causal_conv1d` / flash-linear-attention 的递归状态核,没有对应的 ONNX 算子。
|
|
131
|
+
用这个后端只统一 API 和 `serve`,没有量化加速。
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install 'edgejev[vlm]'
|
|
135
|
+
edgejev build --backend playjev --out ./playjev # 只落配置,不导 ONNX
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
ag = Agent("./playjev")
|
|
140
|
+
ag.system_one("frame.png", { # state 传图片路径 / URL / bytes / PIL / ndarray
|
|
141
|
+
"move": {"type": "choice", "instructions": "Which move should the player make next?",
|
|
142
|
+
"criteria": {"left": "move the paddle left", "right": "move the paddle right",
|
|
143
|
+
"stay": "keep the paddle still"}}})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
4 vCPU CPU 上单次决策约 1.2 s(未装 `causal_conv1d` / `flash-linear-attention` 优化核,
|
|
147
|
+
transformers 回退到参考实现)。返回里多一个 `allowed_mass`:全词表 softmax 落在 K 个字母槽上的质量。
|
|
148
|
+
这个模型的基座是 base 而非 instruct,概率质量天然分散,`allowed_mass` 在 1e-4 量级属正常,
|
|
149
|
+
有判别力的是字母之间的相对排序。实测五个游戏的缩略图,argmax 与分布形状各不相同,
|
|
150
|
+
max(p) 与上游回放记录的真实对局分布(167 步,中位 0.827)落在同一区间。
|
|
151
|
+
|
|
152
|
+
加后端=在 `edgejev/backends/` 写一个模块并在注册表登记。
|
|
153
|
+
|
|
154
|
+
## 基准
|
|
155
|
+
|
|
156
|
+
4 vCPU Intel Xeon Cascade Lake(AVX512-VNNI),`laya-multilingual`。
|
|
157
|
+
|
|
158
|
+
延迟(batch=1,seq≈36):
|
|
159
|
+
|
|
160
|
+
| 精度 | 体积 | 单题 | 三题 | 每题 |
|
|
161
|
+
| :-- | --: | --: | --: | --: |
|
|
162
|
+
| fp32 | 1290 MB | 32.1 ms | 84.1 ms | 28.0 ms |
|
|
163
|
+
| **int8**(默认,per-tensor) | **324 MB** | **15.6 ms** | **44.8 ms** | **14.9 ms** |
|
|
164
|
+
| int8-pc | 325 MB | 16.7 ms | 46.5 ms | 15.5 ms |
|
|
165
|
+
| mixed | 366 MB | 19.2 ms | 53.0 ms | 17.7 ms |
|
|
166
|
+
| uint8-pc | 325 MB | 27.9 ms | 83.4 ms | 27.8 ms |
|
|
167
|
+
|
|
168
|
+
准确率(各 400 条,`edgejev eval --task all --n 400`):
|
|
169
|
+
|
|
170
|
+
| | AG News | emotion |
|
|
171
|
+
| :-- | --: | --: |
|
|
172
|
+
| laya fp32 | 92.8% | 54.0% |
|
|
173
|
+
| laya int8 | 91.0% | 50.0% |
|
|
174
|
+
| Jev 1.13(Vercel AI Gateway) | 85.5% | 61.5% |
|
|
175
|
+
| Jev 1.13(classifier.dev) | 88.0% | 62.7% |
|
|
176
|
+
|
|
177
|
+
`--precision fp32` 与上游 laya(PyTorch)逐位一致,最大概率偏差 `0.00000`。
|
|
178
|
+
|
|
179
|
+
## 已知问题
|
|
180
|
+
|
|
181
|
+
**动态量化的结果依赖 batch。** 激活 scale 在运行时按实际张量算,padding 变了 scale 就变:
|
|
182
|
+
|
|
183
|
+
| | 单条 vs 批量 logits 最大差 |
|
|
184
|
+
| :-- | --: |
|
|
185
|
+
| 上游 laya(PyTorch) | 1e-06 |
|
|
186
|
+
| EdgeJev fp32 ONNX | 0.000e+00 |
|
|
187
|
+
| EdgeJev int8 动态 | 2.43 |
|
|
188
|
+
|
|
189
|
+
`Agent.system_one` 是单请求路径,上面的指标不受影响。自己写批量推理的固定 `batch=1` 或用 fp32。
|
|
190
|
+
`edgejev build` 会检查这一项并在不满足时警告。
|
|
191
|
+
|
|
192
|
+
**`--precision int8-static` 不要用。** 批次无关(漂移 `0.0e+00`)但 MinMax 标定掉到随机水平:
|
|
193
|
+
AG News 25.8%(随机 25%)、emotion 29.8%、延迟 195.5 ms。保留是为了后续换 Percentile / Entropy 标定。
|
|
194
|
+
|
|
195
|
+
**别用 QUInt8。** 同 8 bit 同体积,x86 的 VNNI 只对有符号 int8 有快路径:QUInt8 27.9 ms、QInt8 15.6 ms。
|
|
196
|
+
ARM 走 SDOT,不适用。
|
|
197
|
+
|
|
198
|
+
**保留嵌入表不量化没有收益。** 322M 里 196.6M 是 256k 词表的嵌入表,保留后精度没变(91.0% / 51.5%),
|
|
199
|
+
体积从 325 MB 涨到 915 MB。
|
|
200
|
+
|
|
201
|
+
**fp16 在 x86 CPU 上没有意义。** 没有 `avx512_fp16`,ORT 的 CPU EP 转回 fp32 算;
|
|
202
|
+
且 `onnxconverter_common` 的 fp16 pass 处理不了 dynamo 图里的 `_to_copy` 节点,转出来加载失败。
|
|
203
|
+
|
|
204
|
+
**导出必须用 dynamo。** 旧的 `torch.onnx.export` 把 head 里 `nn.MultiheadAttention` 的 batch/seq
|
|
205
|
+
固化成导出时的形状,换输入长度就抛 Reshape 错,而用同一批次做数值比对时误差 3.87e-06 看不出来。
|
|
206
|
+
`edgejev build` 因此强制跑多形状自检。
|
|
207
|
+
|
|
208
|
+
## 平台
|
|
209
|
+
|
|
210
|
+
| 平台 | Execution Provider |
|
|
211
|
+
| :-- | :-- |
|
|
212
|
+
| Linux / Windows x86 | CPU(AVX512-VNNI / AVX2) |
|
|
213
|
+
| macOS Apple Silicon | CoreML,不支持的算子回退 CPU |
|
|
214
|
+
| Linux ARM | CPU(int8 走 SDOT) |
|
|
215
|
+
|
|
216
|
+
`edgejev info` 看实际选用的;`--provider cpu` 或 `EDGEJEV_PROVIDER=cpu` 强制。
|
|
217
|
+
|
|
218
|
+
## 训练
|
|
219
|
+
|
|
220
|
+
`edgejev.train` 与推理共用同一个渲染器,训练序列和线上请求逐 token 相同。
|
|
221
|
+
|
|
222
|
+
* `losses.py` —— 严格恰当评分规则(log score + spherical;score 型加 ranked probability score)。
|
|
223
|
+
优化校准而不是 argmax 正确。另含 ECE 与可靠性分桶。
|
|
224
|
+
* `data.py` —— 硬标签与软标签统一成目标分布。
|
|
225
|
+
* `model.py` —— 任意 HF 编码器 + 两层 transformer head + marker 打分头,训完可直接被
|
|
226
|
+
`edgejev build --backend laya` 导出。
|
|
227
|
+
|
|
228
|
+
## 许可
|
|
229
|
+
|
|
230
|
+
Apache-2.0。权重与 tokenizer 的许可归上游 [laya](https://huggingface.co/convaiinnovations/laya-multilingual)、
|
|
231
|
+
[mmBERT](https://huggingface.co/jhu-clsp/mmBERT-base)、[PlayJev](https://huggingface.co/OmniJev/PlayJev-0.8B) 所有。
|
edgejev-0.1.0/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# EdgeJev
|
|
2
|
+
|
|
3
|
+
在本地设备上跑 [Jev](https://typesafe.ai) 式的类型化决策。ONNX + INT8,运行时不需要 torch。
|
|
4
|
+
|
|
5
|
+
Jev 不生成文本:给一份 state 和几个带类型的问题,一次前向答完,返回能直接 `if` 的值加一个校准概率。
|
|
6
|
+
EdgeJev 把各家开源复现统一成「转换 → 量化 → 部署」一条路径。
|
|
7
|
+
|
|
8
|
+
4 vCPU Xeon 上 `laya-multilingual`(322M)单题 **15.6 ms**,模型 324 MB。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
还没发到 PyPI,从 GitHub 装:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# uv(推荐,自己管虚拟环境)
|
|
16
|
+
uv tool install "git+https://github.com/yzfly/edgejev"
|
|
17
|
+
|
|
18
|
+
# 或装进项目的虚拟环境
|
|
19
|
+
uv venv && uv pip install "git+https://github.com/yzfly/edgejev"
|
|
20
|
+
|
|
21
|
+
# pipx
|
|
22
|
+
pipx install "git+https://github.com/yzfly/edgejev"
|
|
23
|
+
|
|
24
|
+
# 原生 venv(Debian/Ubuntu 直接 pip 会报 externally-managed-environment)
|
|
25
|
+
python3 -m venv .venv && .venv/bin/pip install "git+https://github.com/yzfly/edgejev"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
可选依赖按需加,`uv pip install "edgejev[build] @ git+https://github.com/yzfly/edgejev"`:
|
|
29
|
+
|
|
30
|
+
| extra | 用途 |
|
|
31
|
+
| :-- | :-- |
|
|
32
|
+
| `build` | `edgejev build` 转 ONNX,需要 torch / transformers / laya |
|
|
33
|
+
| `vlm` | `playjev` 后端,需要 torch / torchvision / transformers / pillow |
|
|
34
|
+
| `train` | `edgejev.train` 训练模块 |
|
|
35
|
+
|
|
36
|
+
不加 extra 时运行时只有 onnxruntime + tokenizers + numpy。
|
|
37
|
+
|
|
38
|
+
## 用法
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
edgejev build --backend laya --out ./jev-int8
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from edgejev import Agent
|
|
46
|
+
|
|
47
|
+
ag = Agent("./jev-int8")
|
|
48
|
+
r = ag.system_one("我的信用卡被扣了两次款,麻烦退一笔。", {
|
|
49
|
+
"dept": {"type": "choice", "instructions": "该转给哪个组?",
|
|
50
|
+
"criteria": {"billing": "支付、扣款、发票、退款",
|
|
51
|
+
"technical": "程序缺陷、报错",
|
|
52
|
+
"sales": "售前咨询、定价"}},
|
|
53
|
+
"urgent": {"type": "noul", "instructions": "这条消息表达了紧急或时间压力"},
|
|
54
|
+
"anger": {"type": "score", "instructions": "客户的不满程度",
|
|
55
|
+
"criteria": ["平静陈述", "有情绪但讲道理", "非常愤怒"]},
|
|
56
|
+
})
|
|
57
|
+
r["answers"]["dept"]["choice"] # 'billing'
|
|
58
|
+
r["answers"]["dept"]["probabilities"] # {'billing': 0.92, 'technical': 0.08, 'sales': 0.00}
|
|
59
|
+
r["answers"]["anger"]["confidence"] # 0.41
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
官方协议端点:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
edgejev serve --model ./jev-int8 --port 8009
|
|
66
|
+
export TYPESAFE_BASE_URL=http://127.0.0.1:8009
|
|
67
|
+
export TYPESAFE_API_KEY=local
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 命令
|
|
71
|
+
|
|
72
|
+
| 命令 | 作用 |
|
|
73
|
+
| :-- | :-- |
|
|
74
|
+
| `edgejev build` | checkpoint → ONNX → 量化 → 多形状自检 |
|
|
75
|
+
| `edgejev serve` | `POST /v1/systemone` |
|
|
76
|
+
| `edgejev eval` | AG News / dair-ai emotion 上跑指标 |
|
|
77
|
+
| `edgejev bench` | 测延迟 |
|
|
78
|
+
| `edgejev info` | provider 与已注册后端 |
|
|
79
|
+
|
|
80
|
+
## 后端
|
|
81
|
+
|
|
82
|
+
| 后端 | 骨干 | 读出方式 | 运行时 | 状态 |
|
|
83
|
+
| :-- | :-- | :-- | :-- | :-- |
|
|
84
|
+
| `laya` | mmBERT-base 322M | 每题一行,读 `[MASK]` 标记位 | ONNX | ✅ |
|
|
85
|
+
| `playjev` | Qwen3.5-0.8B VLM | 画面输入,读最后位置的字母槽 logits | torch | ✅ |
|
|
86
|
+
| `kev` | Qwen + LoRA 0.5B–8B | 多题打包,block-causal,PointerHead | ONNX | 🚧 |
|
|
87
|
+
|
|
88
|
+
`playjev` 走 torch:Qwen3.5 的文本塔是混合线性注意力(`layer_types` 含 `linear_attention`),
|
|
89
|
+
依赖 `causal_conv1d` / flash-linear-attention 的递归状态核,没有对应的 ONNX 算子。
|
|
90
|
+
用这个后端只统一 API 和 `serve`,没有量化加速。
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install 'edgejev[vlm]'
|
|
94
|
+
edgejev build --backend playjev --out ./playjev # 只落配置,不导 ONNX
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
ag = Agent("./playjev")
|
|
99
|
+
ag.system_one("frame.png", { # state 传图片路径 / URL / bytes / PIL / ndarray
|
|
100
|
+
"move": {"type": "choice", "instructions": "Which move should the player make next?",
|
|
101
|
+
"criteria": {"left": "move the paddle left", "right": "move the paddle right",
|
|
102
|
+
"stay": "keep the paddle still"}}})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
4 vCPU CPU 上单次决策约 1.2 s(未装 `causal_conv1d` / `flash-linear-attention` 优化核,
|
|
106
|
+
transformers 回退到参考实现)。返回里多一个 `allowed_mass`:全词表 softmax 落在 K 个字母槽上的质量。
|
|
107
|
+
这个模型的基座是 base 而非 instruct,概率质量天然分散,`allowed_mass` 在 1e-4 量级属正常,
|
|
108
|
+
有判别力的是字母之间的相对排序。实测五个游戏的缩略图,argmax 与分布形状各不相同,
|
|
109
|
+
max(p) 与上游回放记录的真实对局分布(167 步,中位 0.827)落在同一区间。
|
|
110
|
+
|
|
111
|
+
加后端=在 `edgejev/backends/` 写一个模块并在注册表登记。
|
|
112
|
+
|
|
113
|
+
## 基准
|
|
114
|
+
|
|
115
|
+
4 vCPU Intel Xeon Cascade Lake(AVX512-VNNI),`laya-multilingual`。
|
|
116
|
+
|
|
117
|
+
延迟(batch=1,seq≈36):
|
|
118
|
+
|
|
119
|
+
| 精度 | 体积 | 单题 | 三题 | 每题 |
|
|
120
|
+
| :-- | --: | --: | --: | --: |
|
|
121
|
+
| fp32 | 1290 MB | 32.1 ms | 84.1 ms | 28.0 ms |
|
|
122
|
+
| **int8**(默认,per-tensor) | **324 MB** | **15.6 ms** | **44.8 ms** | **14.9 ms** |
|
|
123
|
+
| int8-pc | 325 MB | 16.7 ms | 46.5 ms | 15.5 ms |
|
|
124
|
+
| mixed | 366 MB | 19.2 ms | 53.0 ms | 17.7 ms |
|
|
125
|
+
| uint8-pc | 325 MB | 27.9 ms | 83.4 ms | 27.8 ms |
|
|
126
|
+
|
|
127
|
+
准确率(各 400 条,`edgejev eval --task all --n 400`):
|
|
128
|
+
|
|
129
|
+
| | AG News | emotion |
|
|
130
|
+
| :-- | --: | --: |
|
|
131
|
+
| laya fp32 | 92.8% | 54.0% |
|
|
132
|
+
| laya int8 | 91.0% | 50.0% |
|
|
133
|
+
| Jev 1.13(Vercel AI Gateway) | 85.5% | 61.5% |
|
|
134
|
+
| Jev 1.13(classifier.dev) | 88.0% | 62.7% |
|
|
135
|
+
|
|
136
|
+
`--precision fp32` 与上游 laya(PyTorch)逐位一致,最大概率偏差 `0.00000`。
|
|
137
|
+
|
|
138
|
+
## 已知问题
|
|
139
|
+
|
|
140
|
+
**动态量化的结果依赖 batch。** 激活 scale 在运行时按实际张量算,padding 变了 scale 就变:
|
|
141
|
+
|
|
142
|
+
| | 单条 vs 批量 logits 最大差 |
|
|
143
|
+
| :-- | --: |
|
|
144
|
+
| 上游 laya(PyTorch) | 1e-06 |
|
|
145
|
+
| EdgeJev fp32 ONNX | 0.000e+00 |
|
|
146
|
+
| EdgeJev int8 动态 | 2.43 |
|
|
147
|
+
|
|
148
|
+
`Agent.system_one` 是单请求路径,上面的指标不受影响。自己写批量推理的固定 `batch=1` 或用 fp32。
|
|
149
|
+
`edgejev build` 会检查这一项并在不满足时警告。
|
|
150
|
+
|
|
151
|
+
**`--precision int8-static` 不要用。** 批次无关(漂移 `0.0e+00`)但 MinMax 标定掉到随机水平:
|
|
152
|
+
AG News 25.8%(随机 25%)、emotion 29.8%、延迟 195.5 ms。保留是为了后续换 Percentile / Entropy 标定。
|
|
153
|
+
|
|
154
|
+
**别用 QUInt8。** 同 8 bit 同体积,x86 的 VNNI 只对有符号 int8 有快路径:QUInt8 27.9 ms、QInt8 15.6 ms。
|
|
155
|
+
ARM 走 SDOT,不适用。
|
|
156
|
+
|
|
157
|
+
**保留嵌入表不量化没有收益。** 322M 里 196.6M 是 256k 词表的嵌入表,保留后精度没变(91.0% / 51.5%),
|
|
158
|
+
体积从 325 MB 涨到 915 MB。
|
|
159
|
+
|
|
160
|
+
**fp16 在 x86 CPU 上没有意义。** 没有 `avx512_fp16`,ORT 的 CPU EP 转回 fp32 算;
|
|
161
|
+
且 `onnxconverter_common` 的 fp16 pass 处理不了 dynamo 图里的 `_to_copy` 节点,转出来加载失败。
|
|
162
|
+
|
|
163
|
+
**导出必须用 dynamo。** 旧的 `torch.onnx.export` 把 head 里 `nn.MultiheadAttention` 的 batch/seq
|
|
164
|
+
固化成导出时的形状,换输入长度就抛 Reshape 错,而用同一批次做数值比对时误差 3.87e-06 看不出来。
|
|
165
|
+
`edgejev build` 因此强制跑多形状自检。
|
|
166
|
+
|
|
167
|
+
## 平台
|
|
168
|
+
|
|
169
|
+
| 平台 | Execution Provider |
|
|
170
|
+
| :-- | :-- |
|
|
171
|
+
| Linux / Windows x86 | CPU(AVX512-VNNI / AVX2) |
|
|
172
|
+
| macOS Apple Silicon | CoreML,不支持的算子回退 CPU |
|
|
173
|
+
| Linux ARM | CPU(int8 走 SDOT) |
|
|
174
|
+
|
|
175
|
+
`edgejev info` 看实际选用的;`--provider cpu` 或 `EDGEJEV_PROVIDER=cpu` 强制。
|
|
176
|
+
|
|
177
|
+
## 训练
|
|
178
|
+
|
|
179
|
+
`edgejev.train` 与推理共用同一个渲染器,训练序列和线上请求逐 token 相同。
|
|
180
|
+
|
|
181
|
+
* `losses.py` —— 严格恰当评分规则(log score + spherical;score 型加 ranked probability score)。
|
|
182
|
+
优化校准而不是 argmax 正确。另含 ECE 与可靠性分桶。
|
|
183
|
+
* `data.py` —— 硬标签与软标签统一成目标分布。
|
|
184
|
+
* `model.py` —— 任意 HF 编码器 + 两层 transformer head + marker 打分头,训完可直接被
|
|
185
|
+
`edgejev build --backend laya` 导出。
|
|
186
|
+
|
|
187
|
+
## 许可
|
|
188
|
+
|
|
189
|
+
Apache-2.0。权重与 tokenizer 的许可归上游 [laya](https://huggingface.co/convaiinnovations/laya-multilingual)、
|
|
190
|
+
[mmBERT](https://huggingface.co/jhu-clsp/mmBERT-base)、[PlayJev](https://huggingface.co/OmniJev/PlayJev-0.8B) 所有。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""EdgeJev —— 在自己的设备上跑 Jev 式的类型化决策:快、小、运行时不需要 torch。
|
|
2
|
+
|
|
3
|
+
edgejev build --backend laya --out ./jev-int8
|
|
4
|
+
from edgejev import Agent
|
|
5
|
+
Agent("./jev-int8").system_one(state, questions)
|
|
6
|
+
"""
|
|
7
|
+
from .agent import Agent, load
|
|
8
|
+
|
|
9
|
+
__all__ = ["Agent", "load"]
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""运行时:只依赖 onnxruntime + tokenizers + numpy。跨 Linux / macOS / Windows。"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
from . import backends, post, providers
|
|
7
|
+
from .tokenize import Encoder
|
|
8
|
+
|
|
9
|
+
CONFIG_NAME = "edgejev.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Agent:
|
|
13
|
+
"""本地 System One 决策。
|
|
14
|
+
|
|
15
|
+
from edgejev import Agent
|
|
16
|
+
ag = Agent("./jev-int8")
|
|
17
|
+
ag.system_one("客户被扣了两次款", {
|
|
18
|
+
"dept": {"type": "choice", "instructions": "转给哪个组",
|
|
19
|
+
"criteria": {"billing": "支付扣款", "technical": "程序缺陷"}}})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, model_dir: str, threads: Optional[int] = None,
|
|
23
|
+
provider: Optional[str] = None):
|
|
24
|
+
cfg_path = os.path.join(model_dir, CONFIG_NAME)
|
|
25
|
+
if not os.path.exists(cfg_path):
|
|
26
|
+
raise FileNotFoundError(
|
|
27
|
+
"%s 里没有 %s,这个目录需要用 `edgejev build` 生成。" % (model_dir, CONFIG_NAME))
|
|
28
|
+
with open(cfg_path, encoding="utf-8") as f:
|
|
29
|
+
self.cfg = json.load(f)
|
|
30
|
+
|
|
31
|
+
self.backend = backends.get(self.cfg.get("backend", "laya"))
|
|
32
|
+
self.runtime = self.cfg.get("runtime", "onnx")
|
|
33
|
+
self.temperature = self.cfg.get("temperature", [1.0, 1.0, 1.0])
|
|
34
|
+
self.temperature_by_options = self.cfg.get("temperature_by_options", {})
|
|
35
|
+
self.model_name = self.cfg.get("model_name", "edgejev")
|
|
36
|
+
|
|
37
|
+
if self.runtime == "torch-vlm":
|
|
38
|
+
from .runtimes.torch_vlm import TorchVLM
|
|
39
|
+
self.vlm = TorchVLM(self.cfg["source_model"], template=self.cfg.get("template", "plain"))
|
|
40
|
+
self.provider_note = self.vlm.note
|
|
41
|
+
self.slots = self.vlm.slot_ids(self.backend)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
import onnxruntime as ort
|
|
45
|
+
ids_map = {k: self.cfg[k] for k in self.cfg if k.endswith("_id")}
|
|
46
|
+
ids_map["mask_token"] = self.cfg.get("mask_token", "<mask>")
|
|
47
|
+
self.enc = Encoder.from_file(os.path.join(model_dir, "tokenizer.json"), ids_map)
|
|
48
|
+
|
|
49
|
+
so = ort.SessionOptions()
|
|
50
|
+
so.intra_op_num_threads = threads or (os.cpu_count() or 4)
|
|
51
|
+
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
52
|
+
provs, self.provider_note = providers.detect(provider)
|
|
53
|
+
self.sess = ort.InferenceSession(
|
|
54
|
+
os.path.join(model_dir, self.cfg["onnx_file"]), so, providers=provs)
|
|
55
|
+
|
|
56
|
+
def system_one(self, state: Union[str, dict, list],
|
|
57
|
+
questions: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
|
58
|
+
"""对一份 state 并行评估一组带类型的问题。返回与官方 /v1/systemone 同构的结果。"""
|
|
59
|
+
if not questions:
|
|
60
|
+
raise ValueError("questions 不能为空")
|
|
61
|
+
qids = list(questions.keys())
|
|
62
|
+
qs = [post.to_internal(questions[q]) for q in qids]
|
|
63
|
+
|
|
64
|
+
if self.runtime == "torch-vlm":
|
|
65
|
+
return self._vlm_system_one(state, qids, qs)
|
|
66
|
+
|
|
67
|
+
prep = self.backend.prepare(self.enc, state, qs, self.cfg)
|
|
68
|
+
outputs = self.sess.run(None, prep["feed"])
|
|
69
|
+
logits, act = self.backend.read_logits(outputs, prep)
|
|
70
|
+
|
|
71
|
+
answers = post.assemble(qs, qids, logits, act, prep["k"],
|
|
72
|
+
self.temperature, self.temperature_by_options)
|
|
73
|
+
return {"model": self.model_name, "answers": answers,
|
|
74
|
+
"usage": {"input_tokens": prep["input_tokens"], "output_tokens": 0}}
|
|
75
|
+
|
|
76
|
+
def _vlm_system_one(self, state, qids, qs):
|
|
77
|
+
"""视觉后端:每题一次前向(提示里只放一道题,读最后位置的字母槽)。"""
|
|
78
|
+
import numpy as np
|
|
79
|
+
|
|
80
|
+
image = None
|
|
81
|
+
answers = {}
|
|
82
|
+
for qid, q in zip(qids, qs):
|
|
83
|
+
opts = self.backend.options_from_question(q)
|
|
84
|
+
prompt = self.backend.build_plain_prompt(opts, q["ins"], 1)
|
|
85
|
+
if image is None:
|
|
86
|
+
from .runtimes.torch_vlm import _to_pil
|
|
87
|
+
image = _to_pil(state)
|
|
88
|
+
logits = self.vlm.last_logits(image, prompt)
|
|
89
|
+
probs, allowed = self.backend.readout(logits, self.slots, len(opts))
|
|
90
|
+
conf = round(self.backend.choice_confidence(probs), 4)
|
|
91
|
+
a = post.format_answer(q, np.asarray(probs), conf, None)
|
|
92
|
+
a["allowed_mass"] = round(allowed, 7) # 量级在 1e-4,4 位精度会把不同输入显示成同一个数
|
|
93
|
+
answers[qid] = a
|
|
94
|
+
return {"model": self.model_name, "answers": answers,
|
|
95
|
+
"usage": {"input_tokens": 0, "output_tokens": 0}}
|
|
96
|
+
|
|
97
|
+
predict = system_one
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load(model_dir: str, **kw) -> Agent:
|
|
101
|
+
return Agent(model_dir, **kw)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""后端注册表。新增一个后端=加一个模块并在这里登记。"""
|
|
2
|
+
from . import laya, playjev
|
|
3
|
+
|
|
4
|
+
_REGISTRY = {"laya": laya, "playjev": playjev}
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from . import kev
|
|
8
|
+
_REGISTRY["kev"] = kev
|
|
9
|
+
except Exception: # kev 适配器是可选的
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get(name):
|
|
14
|
+
if name not in _REGISTRY:
|
|
15
|
+
raise ValueError("未知后端 %r,已注册:%s" % (name, ", ".join(sorted(_REGISTRY))))
|
|
16
|
+
return _REGISTRY[name]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def names():
|
|
20
|
+
return sorted(_REGISTRY)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""后端适配器协议。
|
|
2
|
+
|
|
3
|
+
不同的开源 Jev 复现,序列构造和打分头完全不同:
|
|
4
|
+
|
|
5
|
+
* laya —— mmBERT 编码器,每题一个 batch 行,读 [MASK] 标记位的隐状态
|
|
6
|
+
* kev —— Qwen 解码器 + LoRA,多题打包进一条序列,block-causal 掩码,
|
|
7
|
+
PointerHead 用 <decide> 的隐状态去打各个 </opt> 位置的分
|
|
8
|
+
|
|
9
|
+
所以后端必须自己负责:把 (state, questions) 变成模型输入、说明 ONNX 的输入输出签名、
|
|
10
|
+
以及构建期怎么从上游 checkpoint 导出。后处理(温度、熵置信度、答案格式)是共用的。
|
|
11
|
+
"""
|
|
12
|
+
from typing import Any, Dict, List, Protocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Backend(Protocol):
|
|
16
|
+
name: str
|
|
17
|
+
#: ONNX 图的输入名,顺序无关
|
|
18
|
+
input_names: List[str]
|
|
19
|
+
|
|
20
|
+
def prepare(self, enc, state, questions: List[Dict], cfg: Dict) -> Dict[str, Any]:
|
|
21
|
+
"""返回 {"feed": {onnx输入名: ndarray}, "k": [每题的选项数]}。"""
|
|
22
|
+
|
|
23
|
+
def read_logits(self, outputs, meta: Dict) -> Any:
|
|
24
|
+
"""从 ONNX 输出里取出 [问题数, 最大选项数] 的 logits 和可选的 act 概率。"""
|