dsh-prime-memory 0.26.25 → 0.26.27
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.
- package/CHANGELOG.md +26 -0
- package/README.de.md +1 -1
- package/README.en.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.it.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/dist/embedding-child.cjs +14 -13
- package/dist/embedding-token-guard.cjs +74 -20
- package/dist/store/model-catalog.d.ts +15 -7
- package/dist/store/model-catalog.js +15 -7
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# 更新日志(Changelog)
|
|
2
2
|
|
|
3
|
+
## [0.26.27] — 2026-11-02
|
|
4
|
+
|
|
5
|
+
> #22 报告人在 macOS/M5 上回验了 `0.26.25`:**三个原致死档不再 SIGTRAP、护栏确实接上**(这验证了主线结论),但翻出一个**比原缺陷更贴近真实负载**的问题,本版纳入他的二分实测。
|
|
6
|
+
|
|
7
|
+
- **`HARD_CAP_TOKENS` 2048 → 1536:预算原本只对了「单发」,而本仓真实负载是「连发」**。报告人二分实测(Electron-node,`5×5000` 中文字符串行):**512/1024 全过、1900/1960 全过、1990/2000/2010/2016/2020/2040/2048 均「第 1 条成功、第 2 条 SIGTRAP」**;而单发致死线是 ≈2700。两者不同源的机理止得住我的原分析:**BFCArena 高水位不回落**(我先前已测到 6 轮大推理后 RSS plateau 不降),第二条进来时第一份 T² 张量尚未回收完,**新块与旧块共存瞬间的峰值**才是真正的天花板。
|
|
8
|
+
- **为什么这条要紧(已实地核实,不是直按报告人的话)**:`src/pipeline/graph.ts:134-137` 正是 `for (const n of touched) { … await embed(nodeEmbedText(n)) … }` —— 逐条串行且**单条请求**,一个投影批里常有 5~8 个节点,其中两条长就命中。所以原issue 的「每次投影必崩」在 2048 下只降到「命中≥2 条长文本时崩」,而**本刀已声明不做自愈**,命中仍是崩一次废到重启。1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值,他也给了 1024 选项;取 1536 是为了不一刀牺牲 bge-m3 长文本的召回语义)。
|
|
9
|
+
- **T1 脚本新增「连发」维度**(`scripts/verify-token-guard-realmodel.mjs --serial N`):同一段 5000 字符在**同一 pipeline 实例**里串行喂 5 次,断言**每一轮实测 seq 均 ≤ 预算且各轮一致**。本机(Windows/纯 node)不会 trap,所以这只能证「预算对连发一致成立」,不能证「连发不崩」(后者只能 macOS)。实测:`seqs=1535,1535,1535,1535,1535`(预算 1536);T3 峰值从 1469MB 降至 **1235MB**(无护栏仍 4168MB)。
|
|
10
|
+
- **收干自己造下的魔数漂移面**(这是 `0.26.25/26` 留下的):测试与脚本里原本**把 2048 当字面量贴了 8 处**,改常量时全变成假红/假绿。现一律从 `HARD_CAP_TOKENS` 推导,真模型脚本也从 `dist/model-catalog.js` 读默认值;哨兵断言同时禁止 `1536|2048|2700` 出现在子进程/主进程层。**诚实记录:改完从常量推导后,我立刻发现自己两处算错而实现的断言**(用魔数时我把错误算成了和实现一致的值,所以“看起来绿”),现在改成断**契约性质**(收到预算内 + 真的截了 + 首轮不多试)而不是断精确切片长度。
|
|
11
|
+
- **文档同步**:9 语 README 的 `embedding.maxInputTokens` 行由「硬上限 2048」改为 1536;`src/config.ts` 对该键的描述不含具体数字(避免下一处漂移)。
|
|
12
|
+
- 另接受报告人一个诊断修正:他指出 `tokensMax` 在 `0.26.25` 恒为 0、且 `Array.isArray(shape)` 分支「永远进不去」——与我在 `0.26.26` 独立查到的同一缺陷一致(他比我先跑出这个结论,他是从外部拿真 tokenizer 量的)。
|
|
13
|
+
- **测试**:`tests/embedding-token-guard.test.ts` 保持 **29 例**(断言形式从魔数改为常量推导);全量 **1933 例 / 191 文件**全绿,typecheck 0 error、lint 0 error、build/build:smoke/smoke/verify-catalog 均 0。公开配置面无破坏性变更(仅默认推导出的预算从 2048 收紧到 1536,显式设过 `maxInputTokens` 的用户不受影响)。
|
|
14
|
+
|
|
15
|
+
## [0.26.26] — 2026-11-02
|
|
16
|
+
|
|
17
|
+
> `0.26.25` 把 #22 的 token 护栏落地了,但把**最关键的那条验证当成了「已接上报」而非「已实测」**。本版用真 bge-m3 把它跑完,跑出了三个真实缺陷并逐个修掉。
|
|
18
|
+
|
|
19
|
+
- **T1 从「上报机制」补成「真模型实证」**(新增 `scripts/verify-token-guard-realmodel.mjs` + `scripts/_t3-runner.mjs`,可选跑、不进 CI——需本机 runtime 与 bge-m3)。在真 bge-m3 上实测:**5000/4000/3800 中文字符(报告里全部 SIGTRAP 的那三档)截断后进模型的序列长度均为 2047 ≤ 预算 2048**;无护栏时实测 **5003**;截断前后向量不同(排除「只报了个数、实际没截」)。护栏留位从「假设」变成「事实」。
|
|
20
|
+
- **修掉三个被真模型才抓得出的缺陷**(桩测试永远测不到):
|
|
21
|
+
1. **`tokensMax` 恒为 -1,等于没上报**——原实现从输出张量取 `.shape`,而实测发现 transformers 的 Tensor **根本没有 `.shape`**(叫 `.dims`),且池化后输出是 `[1024]` 一维、里面**不存在 seq** 这个维度。现改为截断时按库自己的口径(`padding:true, truncation:true`)实测并上报,脚本还拿两个独立取法互验(实测 2047 == 2047)。
|
|
22
|
+
2. **「预算留 2 位」是侥幸不是保证**——真测:切 2046 个 id → decode → 重分词得 2045 token,+2 special = 2047,预算 2048 **只剩 1 位余量**;BPE/Unigram 的切片→解码→再编码不保证同序列,换一段文本就可能多漂而越界。现改为**由实测超出量逐轮递降收紧**(每轮重编码取真 seq,严格变短,至多 3 轮;量不出来就报 -1 而不是猜)。
|
|
23
|
+
3. **基准不能只看 encode 的 id 数**——5000 中文字符的 `encode` 长 5000,而 pipeline 实际会送进 5003;已改为取两者较大值作基准。
|
|
24
|
+
- **消除一个我自己引入的测试污染**:本为取 `shrinkToBudget` 而让测试 `require('embedding-child.cjs')`,会把它的顶层 `process.on('uncaughtException')`(处理器会 `process.exit(1)`)与 `message` 监听装进 vitest 的 worker——**实测同 worker 的 `tests/memory-md-sync.test.ts` 因此无端转红(单跑绿、全量红)**。现恢复 child 的 fork-only 契约(只加一条文件头说明为何不可被 require),截断逻辑住在无副作用纯模块,对 child 仅做字面接线断言 + 产物 smoke,真行为由真模型脚本负责。
|
|
25
|
+
- 另记一条**既有偶发失败**(非本次引入、本次未修):`memory-md-sync.test.ts` 在全量并行下会间歇红(恢复活跃时 `.md.disable` 后缀未及时消失)。已用基线 worktree 对照:**不含本批改动的 `f3c1c07` 连跑 5 次也红过 1 次**,而本树连跑 3 次全绿,故不属本刀回归;具体原因是该用例的异步写盘与 `existsSync` 同步断言竞速,待另案。
|
|
26
|
+
- smoke 的两条产物断言从「贴字面量」改为「断意图」(上一版贴了 `module.exports = { shrinkToBudget }` 与 `maxTokens - 2`,实现一改就误报)。
|
|
27
|
+
- **测试**:`tests/embedding-token-guard.test.ts` 24 → **29 例**(新增逐轮递降、量不出来降级报 -1、`seqLenOf` 同时吃数组/张量两形等);全量 **1933 例 / 191 文件**全绿(连跑 3 次),typecheck 0 error、lint 0 error(4 条既有 warning)、build/build:smoke/smoke/verify-catalog 均 0。公开配置面无破坏性变更(`maxInputTokens`/`maxInputChars` 语义与默认值未动)。
|
|
28
|
+
|
|
3
29
|
## [0.26.25] — 2026-11-02
|
|
4
30
|
|
|
5
31
|
- **#22 根治:本地嵌入新增 token 维度护栏**。报告实测同一 5000 中文字符(≈3651 token)在纯 node 跑通(峰值 RSS **4.7GB**)、在宿主 Electron-node 被 **SIGTRAP 静默打死**(796ms、峰值仅 1.05GB),且崩溃瞬间 writable resident 只 3381K、机器 24GB 未吃满——**不是 OOM,是「单笔大块对齐分配被拒」**。本轮在本机(Windows/纯 node)实测内存峰值严格 **O(T²)**(750/1500/3003 token → ΔRSS +106/437/1635 MB,比 1:4.1:15.4 对应 T² 的 1:4:16),机制是 attention 张量 `[1,heads,T,T]`;外推 3651 token ≈4.6GB,与报告的 4.7GB 跨平台互证。**T 是唯一可控自变量,降 token 预算同时是崩溃护栏和内存护栏。**
|
package/README.de.md
CHANGED
|
@@ -323,7 +323,7 @@ Drei Embedding-Quellen: **aus** (Standard, reine BM25-Schlüsselwortsuche), **re
|
|
|
323
323
|
| `embedding.model` | leer | Embedding-Modellname |
|
|
324
324
|
| `embedding.dimensions` | `0` | Vektordimension (bei Aktivierung Pflicht, muss zur Modellausgabe passen) |
|
|
325
325
|
| `embedding.maxInputChars` | `5000` | Maximale Zeichen je Text (bei Überlänge abgeschnitten) |
|
|
326
|
-
| `embedding.maxInputTokens` | `0` | **Token**-Budget pro Text für lokale Einbettung: `0` (Standard) = min(Modellfenster, harte Grenze
|
|
326
|
+
| `embedding.maxInputTokens` | `0` | **Token**-Budget pro Text für lokale Einbettung: `0` (Standard) = min(Modellfenster, harte Grenze 1536); >0 = explizit, bei Überschreitung geklemmt inkl. warn. Andere Rolle als die Zeile oben – das Zeichenlimit betrifft nur Remote-Modus & IPC-Größe, die lokale Absturz-/Speichergrenze muss in Tokens stehen (#22) |
|
|
327
327
|
| `embedding.timeoutMs` | `10000` | Timeout eines einzelnen Embedding-Aufrufs (ms) |
|
|
328
328
|
| `embedding.allowLocalModels` | `true` | Lokale Embedding-Stufe erlauben (Deployment-Obergrenze: ausgeschaltet kann die Einstellungsseite keine Modelle laden und nicht auf Lokal schalten) |
|
|
329
329
|
| `embedding.mirror` | `https://hf-mirror.com` | Wurzeladresse des Download-Spiegels für lokale Modelle (zurücksetzbar auf das offizielle `https://huggingface.co`) |
|
package/README.en.md
CHANGED
|
@@ -330,7 +330,7 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
|
|
|
330
330
|
| `embedding.model` | empty | embedding model name |
|
|
331
331
|
| `embedding.dimensions` | `0` | Vector dimensions (required when enabled; must match model output) |
|
|
332
332
|
| `embedding.maxInputChars` | `5000` | Max characters per text (overlong inputs truncated) |
|
|
333
|
-
| `embedding.maxInputTokens` | `0` | Per-text **token** budget for local embedding: `0` (default) = min(model window, hard cap
|
|
333
|
+
| `embedding.maxInputTokens` | `0` | Per-text **token** budget for local embedding: `0` (default) = min(model window, hard cap 1536); >0 = explicit, clamped with a warn above the cap. Not the same guard as the row above — the character cap only bounds the remote tier and IPC size, while local crash/memory limits must be in tokens (#22) |
|
|
334
334
|
| `embedding.timeoutMs` | `10000` | Per-call embedding timeout (ms) |
|
|
335
335
|
| `embedding.allowLocalModels` | `true` | Allow the local embedding tier (deployment ceiling; when off, no model downloads and no local tier in settings) |
|
|
336
336
|
| `embedding.mirror` | `https://hf-mirror.com` | Download mirror root for local models (can be changed back to `https://huggingface.co`) |
|
package/README.es.md
CHANGED
|
@@ -324,7 +324,7 @@ Los ajustes que pisan los valores por defecto se escriben en el `cordis.patch.ym
|
|
|
324
324
|
| `embedding.model` | vacío | Nombre del modelo de embeddings |
|
|
325
325
|
| `embedding.dimensions` | `0` | Dimensión de los vectores (obligatoria si está activada, debe coincidir con la salida del modelo) |
|
|
326
326
|
| `embedding.maxInputChars` | `5000` | Número máximo de caracteres por texto (recorte si se supera) |
|
|
327
|
-
| `embedding.maxInputTokens` | `0` | Presupuesto de **tokens** por texto para la incrustación local: `0` (pred.) = min(ventana del modelo, límite
|
|
327
|
+
| `embedding.maxInputTokens` | `0` | Presupuesto de **tokens** por texto para la incrustación local: `0` (pred.) = min(ventana del modelo, límite 1536); >0 = explícito, recortado con warn si excede. No es el mismo límite que la fila de arriba: el de caracteres solo cubre el modo remoto y el tamaño de IPC, y la protección local contra fallos/memoria debe ir en tokens (#22) |
|
|
328
328
|
| `embedding.timeoutMs` | `10000` | Tiempo máximo de una llamada de embeddings (ms) |
|
|
329
329
|
| `embedding.allowLocalModels` | `true` | Permite el modo de embeddings local (techo de despliegue: desactivado, la página de ajustes no puede descargar modelos ni pasar a local) |
|
|
330
330
|
| `embedding.mirror` | `https://hf-mirror.com` | Raíz del espejo de descarga de modelos locales (se puede devolver al oficial `https://huggingface.co`) |
|
package/README.fr.md
CHANGED
|
@@ -322,7 +322,7 @@ Les réglages par-dessus les valeurs par défaut s'écrivent dans le `cordis.pat
|
|
|
322
322
|
| `embedding.model` | vide | Nom du modèle d'embedding |
|
|
323
323
|
| `embedding.dimensions` | `0` | Dimension des vecteurs (obligatoire si activé, doit correspondre à la sortie du modèle) |
|
|
324
324
|
| `embedding.maxInputChars` | `5000` | Nombre maximal de caractères par texte (troncature au-delà) |
|
|
325
|
-
| `embedding.maxInputTokens` | `0` | Budget de **jetons** par texte pour l'intégration locale : `0` (défaut) = min(fenêtre du modèle, plafond
|
|
325
|
+
| `embedding.maxInputTokens` | `0` | Budget de **jetons** par texte pour l'intégration locale : `0` (défaut) = min(fenêtre du modèle, plafond 1536) ; >0 = explicite, réduit avec un warn s'il dépasse. Ce n'est pas la même limite que la ligne au-dessus : celle en caractères ne borne que le mode distant et la taille IPC, la protection locale doit être en jetons (#22) |
|
|
326
326
|
| `embedding.timeoutMs` | `10000` | Délai maximal d'un appel d'embedding (ms) |
|
|
327
327
|
| `embedding.allowLocalModels` | `true` | Autorise le mode embedding local (plafond de déploiement : désactivé, la page de paramètres ne peut plus télécharger de modèles ni passer en local) |
|
|
328
328
|
| `embedding.mirror` | `https://hf-mirror.com` | Racine du miroir de téléchargement des modèles locaux (ramenérable à l'officiel `https://huggingface.co`) |
|
package/README.it.md
CHANGED
|
@@ -324,7 +324,7 @@ Le impostazioni che scavalcano i predefiniti si scrivono nel `cordis.patch.yml`
|
|
|
324
324
|
| `embedding.model` | vuoto | Nome del modello d'embedding |
|
|
325
325
|
| `embedding.dimensions` | `0` | Dimensione dei vettori (obbligatoria se attiva, deve corrispondere all'output del modello) |
|
|
326
326
|
| `embedding.maxInputChars` | `5000` | Numero massimo di caratteri per testo (troncamento oltre) |
|
|
327
|
-
| `embedding.maxInputTokens` | `0` | Budget di **token** per testo per l'embedding locale: `0` (default) = min(finestra del modello, limite
|
|
327
|
+
| `embedding.maxInputTokens` | `0` | Budget di **token** per testo per l'embedding locale: `0` (default) = min(finestra del modello, limite 1536); >0 = esplicito, ridotto con un warn se oltre il limite. Non è lo stesso limite della riga sopra: quello in caratteri copre solo modalità remota e dimensione IPC, la protezione locale contro cadute/memoria deve essere in token (#22) |
|
|
328
328
|
| `embedding.timeoutMs` | `10000` | Timeout di una singola chiamata d'embedding (ms) |
|
|
329
329
|
| `embedding.allowLocalModels` | `true` | Consente la modalità d'embedding locale (tetto di distribuzione: disattivata, la pagina impostazioni non può scaricare modelli né passare in locale) |
|
|
330
330
|
| `embedding.mirror` | `https://hf-mirror.com` | Radice del mirror di scaricamento dei modelli locali (riportabile all'ufficiale `https://huggingface.co`) |
|
package/README.ja.md
CHANGED
|
@@ -274,7 +274,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
|
|
|
274
274
|
| `embedding.model` | 空 | 埋め込みモデル名 |
|
|
275
275
|
| `embedding.dimensions` | `0` | ベクトル次元(有効時必須) |
|
|
276
276
|
| `embedding.maxInputChars` | `5000` | 単テキスト最大文字数(超長は切り捨て) |
|
|
277
|
-
| `embedding.maxInputTokens` | `0` | ローカル埋め込みの 1 件あたり **トークン** 予算:`0`(既定)= モデル窓と上限
|
|
277
|
+
| `embedding.maxInputTokens` | `0` | ローカル埋め込みの 1 件あたり **トークン** 予算:`0`(既定)= モデル窓と上限 1536 の小さい方、>0 = 明示値(超過は clamp + warn)。上の行とは役割が異なる——文字上限はリモートモードと IPC サイズのみを制限し、ローカルのクラッシュ/メモリ防護はトークン単位が必須(#22) |
|
|
278
278
|
| `embedding.timeoutMs` | `10000` | 単回埋め込み呼び出しタイムアウト(ms) |
|
|
279
279
|
| `embedding.allowLocalModels` | `true` | ローカル埋め込み档を許可(デプロイ上限) |
|
|
280
280
|
| `embedding.mirror` | `https://hf-mirror.com` | ローカルモデルダウンロードミラー根 |
|
package/README.ko.md
CHANGED
|
@@ -274,7 +274,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
|
|
|
274
274
|
| `embedding.model` | 빈값 | 임베딩 모델명 |
|
|
275
275
|
| `embedding.dimensions` | `0` | 벡터 차원(활성 시 필수) |
|
|
276
276
|
| `embedding.maxInputChars` | `5000` | 단텍스트 최대 문자 수(초장 잘림) |
|
|
277
|
-
| `embedding.maxInputTokens` | `0` | 로컬 임베딩 1건당 **토큰** 예산: `0`(기본) = 모델 창과 상한
|
|
277
|
+
| `embedding.maxInputTokens` | `0` | 로컬 임베딩 1건당 **토큰** 예산: `0`(기본) = 모델 창과 상한 1536 중 작은 값, >0 = 명시값(초과 시 clamp + warn). 위 행과 역할이 다름 — 문자 상한은 원격 모드와 IPC 크기만 제한하고, 로컬 충돌/메모리 방지는 토큰 단위가 필수(#22) |
|
|
278
278
|
| `embedding.timeoutMs` | `10000` | 단회 임베딩 호출 타임아웃(ms) |
|
|
279
279
|
| `embedding.allowLocalModels` | `true` | 로컬 임베딩档 허용(배포 상한) |
|
|
280
280
|
| `embedding.mirror` | `https://hf-mirror.com` | 로컬 모델 다운로드 미러 루트 |
|
package/README.md
CHANGED
|
@@ -342,7 +342,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
342
342
|
| `embedding.model` | 空 | embedding 模型名 |
|
|
343
343
|
| `embedding.dimensions` | `0` | 向量维度(启用时必填,须与模型输出一致) |
|
|
344
344
|
| `embedding.maxInputChars` | `5000` | 单条文本最大字符数(超长截断) |
|
|
345
|
-
| `embedding.maxInputTokens` | `0` | 本地嵌入的单条 **token** 预算:`0`(默认)= 按模型窗口与硬上限
|
|
345
|
+
| `embedding.maxInputTokens` | `0` | 本地嵌入的单条 **token** 预算:`0`(默认)= 按模型窗口与硬上限 1536 自动取小;>0 = 显式预算,超上限会被 clamp 并记 warn。与上一行不是同类护栏——字符帽只管远程档与 IPC 体积,本地推理的崩溃/内存护栏必须按 token(#22) |
|
|
346
346
|
| `embedding.timeoutMs` | `10000` | 单次 embedding 调用超时(ms) |
|
|
347
347
|
| `embedding.allowLocalModels` | `true` | 允许本地嵌入档(部署上限:关闭后设置页不能下载模型、不能切本地档) |
|
|
348
348
|
| `embedding.mirror` | `https://hf-mirror.com` | 本地模型下载镜像根地址(可改回官方 `https://huggingface.co`) |
|
package/README.ru.md
CHANGED
|
@@ -324,7 +324,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
|
|
|
324
324
|
| `embedding.model` | пусто | Имя модели эмбеддингов |
|
|
325
325
|
| `embedding.dimensions` | `0` | Размерность векторов (при включении обязательна, должна совпадать с выходом модели) |
|
|
326
326
|
| `embedding.maxInputChars` | `5000` | Максимум символов на один текст (сверх — усечение) |
|
|
327
|
-
| `embedding.maxInputTokens` | `0` | Бюджет **токенов** на один текст для локальных эмбеддингов: `0` (умолч.) = min(окно модели, жёсткий лимит
|
|
327
|
+
| `embedding.maxInputTokens` | `0` | Бюджет **токенов** на один текст для локальных эмбеддингов: `0` (умолч.) = min(окно модели, жёсткий лимит 1536); >0 = явное значение, при превыщении усекается с warn. Это не тот же лимит, что строкой выше: символьный ограничивает только удалённый режим и размер IPC, а локальная защита от падений/памяти должна считаться в токенах (#22) |
|
|
328
328
|
| `embedding.timeoutMs` | `10000` | Таймаут одного вызова эмбеддинга (мс) |
|
|
329
329
|
| `embedding.allowLocalModels` | `true` | Разрешить локальный режим эмбеддингов (потолок развёртывания: выключено — страница настроек не может скачивать модели и переключаться в локальный) |
|
|
330
330
|
| `embedding.mirror` | `https://hf-mirror.com` | Корень зеркала загрузки локальных моделей (можно вернуть на официальный `https://huggingface.co`) |
|
package/dist/embedding-child.cjs
CHANGED
|
@@ -31,6 +31,13 @@ const { createRequire } = require('node:module');
|
|
|
31
31
|
// 「无 IPC 就 exit」守卫,被测试 require 会把副作用带进来。截断逻辑本身必须零模型可验。
|
|
32
32
|
const { shrinkToBudget } = require('./embedding-token-guard.cjs');
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 本文件是 **fork 专用入口**:顶层的 `process.on('message'/'disconnect'/'uncaughtException')`
|
|
36
|
+
* 与「无 IPC 就 exit」守卫都是执行体行为,**不得被测试 require**(给宿主进程装全局监听会
|
|
37
|
+
* 污染同进程的其他代码:实测过给 vitest worker 装 `uncaughtException` 后,同 worker 的
|
|
38
|
+
* `memory-md-sync` 用例无端转红)。需要零模型可验的逻辑一律放 `embedding-token-guard.cjs`。
|
|
39
|
+
*/
|
|
40
|
+
|
|
34
41
|
// 必须由 fork 启动(需要 IPC channel);裸 node 执行时静默退出,不报 SyntaxError、不永挂。
|
|
35
42
|
if (typeof process.send !== 'function') {
|
|
36
43
|
console.error('[embedding-child] 未经 fork 启动,无 IPC 通道,退出');
|
|
@@ -138,21 +145,15 @@ async function pump() {
|
|
|
138
145
|
try {
|
|
139
146
|
// 字符粗筛(IPC 体积) + token 精截(#22 崩溃/内存护栏);截断只在推理前,run() 同步不可中断
|
|
140
147
|
const screened = job.text.slice(0, MAX_INPUT_CHARS);
|
|
141
|
-
const
|
|
142
|
-
const result = await extractor([
|
|
148
|
+
const cut = shrinkToBudget(screened, MAX_INPUT_TOKENS, tokenizer);
|
|
149
|
+
const result = await extractor([cut.text], { pooling: POOLING, normalize: true });
|
|
143
150
|
const data = result[0] && result[0].data;
|
|
144
151
|
job.request.vectors[job.index] = data instanceof Float32Array ? data : new Float32Array(data);
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const shape = result[0] && result[0].shape;
|
|
151
|
-
if (Array.isArray(shape) && shape.length >= 3) real = Number(shape[1]) || -1;
|
|
152
|
-
} catch {
|
|
153
|
-
/* shape 不可读不致命 */
|
|
154
|
-
}
|
|
155
|
-
if (real > job.request.tokensMax) job.request.tokensMax = real;
|
|
152
|
+
// tokensMax 用 cut.seq(按库口径实测将要进模型的 seq 长度,含 special token)。
|
|
153
|
+
// 曾试过从**输出**张量取形状——真 bge-m3 实测证明两事:① Tensor 上根本没有 `.shape`
|
|
154
|
+
// (属性叫 `.dims`),② 池化后输出是一维向量,里面不存在 seq 维——旧写法永远拿不到值。
|
|
155
|
+
// 不可证时为 -1(不是 0),不拿假数据当护栏证据。
|
|
156
|
+
if (cut.seq > job.request.tokensMax) job.request.tokensMax = cut.seq;
|
|
156
157
|
job.request.remaining -= 1;
|
|
157
158
|
settle(job.request);
|
|
158
159
|
} catch (err) {
|
|
@@ -5,37 +5,91 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* 把一条文本裁到 token
|
|
8
|
+
* 把一条文本裁到 **实测** token 预算内。返回 `{ text, seq }`:
|
|
9
|
+
* - `text` = 应送进 pipeline 的文本;
|
|
10
|
+
* - `seq` = 按库自己的口径(`padding:true, truncation:true`,含 special token)**实测将要**进模型
|
|
11
|
+
* 的序列长度;不可测时为 -1(-1 是「不可证」,不是 0)。
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
* 的 run()
|
|
12
|
-
* 是 #11
|
|
13
|
+
* 为什么截断只能在子进程做:tokenizer 只在加载后的 pipeline 对象上可达,且 onnxruntime
|
|
14
|
+
* 的 run() 是同步阻塞调用——单条内部不可中断,预检只能放推理前。又因为「子进程隔离」本身
|
|
15
|
+
* 是 #11(libvips-42.dll 同进程撞名)的修复,不得为省一次 IPC 把 tokenize 挪回主线程。
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
* pipeline
|
|
17
|
+
* 为什么不靠「固定留几个位」这种推断(真 bge-m3 实测已证伪):
|
|
18
|
+
* 切 ids → decode 回文本 → pipeline 再分词,**不保证得到同一序列**——在当时的 2048 预算下实测:
|
|
19
|
+
* 切 2046 个 id 解码后重分词得 2045 token,加 2 个 special token = **2047**,**只剩 1 位余量**。
|
|
20
|
+
* 换一段文本就可能多漂一位而越界,所以下限靠**实测逐轮收紧**,不靠常数猜测。
|
|
16
21
|
*
|
|
17
|
-
* 不做「短文本跳过 tokenize
|
|
18
|
-
* 英文≈0.27
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* 不做「短文本跳过 tokenize」的优化:字符→token 比值按词表而变(CJK≈1、emoji/泰文>1.5、
|
|
23
|
+
* 英文≈0.27),任何启发式下限都可能放走真超限的文本(实测反例:3800 中文字符 ×0.5=1900
|
|
24
|
+
* 会跳过,而它实际约 2774 token,正落在 #22 致死档),而 tokenize 只占单次推理 ≈2.4%。
|
|
25
|
+
* 护栏上省 2.4% 不值一个未验证的跳跃。
|
|
21
26
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
27
|
+
* 任何一步不可用(transformers 未挂 tokenizer、不认选项、decode 回空串、抛错)一律
|
|
28
|
+
* **退回粗筛原文保持旧行为**:护栏不得把能跑的链路搞坏。
|
|
29
|
+
*
|
|
30
|
+
* 成本:每轮多一个 encode(实测占单次推理 ≈2.4%);至多重试 ROUNDS 轮。
|
|
24
31
|
*/
|
|
32
|
+
const ROUNDS = 3;
|
|
33
|
+
|
|
34
|
+
/** 按库口径量一次真实序列长度(pipeline 内部就是 `this.tokenizer(texts,{padding:true,truncation:true})`)。 */
|
|
35
|
+
function seqLenOf(tok, text) {
|
|
36
|
+
const enc = tok(text, { padding: true, truncation: true });
|
|
37
|
+
const ids = enc && enc.input_ids;
|
|
38
|
+
if (!ids) return -1;
|
|
39
|
+
if (Array.isArray(ids)) return ids.length;
|
|
40
|
+
if (Array.isArray(ids.dims) && ids.dims.length >= 2) return ids.dims[1];
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
function shrinkToBudget(raw, maxTokens, tok) {
|
|
26
|
-
if (!tok || !(maxTokens > 0)) return raw;
|
|
45
|
+
if (!tok || !(maxTokens > 0)) return { text: raw, seq: -1 };
|
|
27
46
|
try {
|
|
28
47
|
// 实测陷阱:return_tensor 缺省为 true,直取 .input_ids 得到的是 _Tensor(.length undefined
|
|
29
48
|
// 且不抛错) -> 必须显式 return_tensor:false
|
|
30
49
|
const ids = tok.encode(raw, { return_tensor: false });
|
|
31
|
-
if (!Array.isArray(ids)
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
if (!Array.isArray(ids)) return { text: raw, seq: -1 };
|
|
51
|
+
// 基准长度用原文的库口径实测值(含 special token)。注意:**不能**只看 encode 的 id 数——
|
|
52
|
+
// 真 bge-m3 实测 5000 中文字符的 encode 长 5000,而 pipeline 实际会送进 5003(库自己补的
|
|
53
|
+
// special token 与词表边界),所以只有库口径才是「到底进了多少」的那个数。
|
|
54
|
+
let base = ids.length;
|
|
55
|
+
let direct = -1;
|
|
56
|
+
try {
|
|
57
|
+
direct = seqLenOf(tok, raw);
|
|
58
|
+
} catch {
|
|
59
|
+
direct = -1;
|
|
60
|
+
}
|
|
61
|
+
if (direct >= 0) base = Math.max(base, direct);
|
|
62
|
+
if (base <= maxTokens) return { text: raw, seq: direct >= 0 ? direct : -1 };
|
|
63
|
+
|
|
64
|
+
// 递降量由**上一轮实测的超出量**推出,而不是猜常数:切片长度是编码的单调函数,
|
|
65
|
+
// 所以每轮必比上一轮更短;再编码一次是为了拿到那个长度的真 seq,不做二次推断。
|
|
66
|
+
// 起点先留 2 位(special token 的下界,真测=+2),之后按实测超出量继续收。
|
|
67
|
+
let text = raw;
|
|
68
|
+
let seq = -1;
|
|
69
|
+
let prevLen = ids.length;
|
|
70
|
+
let cut = Math.max(1, ids.length - (base - maxTokens) - 2);
|
|
71
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
72
|
+
if (cut >= prevLen) cut = Math.max(1, prevLen - 1); // 保证严格变短
|
|
73
|
+
const kept = ids.slice(0, cut);
|
|
74
|
+
// 实测陷阱:decode 缺省 skip_special_tokens=false,会把 [CLS]/[SEP] 编回文本再二次分词
|
|
75
|
+
const shrunk = tok.decode(kept, { skip_special_tokens: true });
|
|
76
|
+
if (typeof shrunk !== 'string' || shrunk.length === 0) break; // decode 不可用 -> 停止递降
|
|
77
|
+
text = shrunk;
|
|
78
|
+
const reIds = tok.encode(text, { return_tensor: false });
|
|
79
|
+
if (!Array.isArray(reIds) || reIds.length === 0) return { text: raw, seq: -1 };
|
|
80
|
+
seq = seqLenOf(tok, text);
|
|
81
|
+
if (seq < 0) return { text: raw, seq: -1 }; // 量不了 -> 降级(不送无法审计的截断文本)
|
|
82
|
+
if (seq <= maxTokens) return { text, seq };
|
|
83
|
+
prevLen = reIds.length;
|
|
84
|
+
// 递降量 = 实测超出量 + 2 位余量(经验值,但正确性不依赖它:依赖上文的严格变短)
|
|
85
|
+
cut = Math.max(1, prevLen - (seq - maxTokens) - 2);
|
|
86
|
+
}
|
|
87
|
+
// 逐轮后仍超(极端漂移):宁报 -1(不可证)也不报一个超限的数当安全证据。
|
|
88
|
+
if (seq > maxTokens) return { text, seq: -1 };
|
|
89
|
+
return { text, seq };
|
|
36
90
|
} catch {
|
|
37
|
-
return raw; // 降级:不截断,保持旧行为
|
|
91
|
+
return { text: raw, seq: -1 }; // 降级:不截断,保持旧行为
|
|
38
92
|
}
|
|
39
93
|
}
|
|
40
94
|
|
|
41
|
-
module.exports = { shrinkToBudget };
|
|
95
|
+
module.exports = { shrinkToBudget, seqLenOf };
|
|
@@ -50,21 +50,29 @@ export declare function catalogById(id: string): CatalogEntry | undefined;
|
|
|
50
50
|
/**
|
|
51
51
|
* 本地 ONNX 推理的单条 token 上限(**唯一硬编码源**,issue #22)。
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* 为什么是 1536 而不是 2048:本值受**连发**致死线约束,不是单发线。
|
|
54
|
+
* #22 报告人在 macOS/M5 上二分实测(Electron-node,5×5000 中文字符串行):
|
|
55
|
+
* 512/1024 全过、1900/1960 全过、**1990/2000/2010/2016/2020/2040/2048 均第 2 条即 SIGTRAP**
|
|
56
|
+
* —— 即**连发致死线落在 1960~1990**,而单发致死线是 ≈2700(issue 初测)。
|
|
57
|
+
* 两者不同源的机理:BFCArena 高水位不回落,第二条进来时第一份 T² 张量尚未回收完,
|
|
58
|
+
* 新块与旧块共存瞬间的峰值才是真正的天花板。
|
|
59
|
+
*
|
|
60
|
+
* 而**真实负载就是连发**:`pipeline/graph.ts` 对 touched 节点「`for … await embed(…)`」
|
|
61
|
+
* 逐条串行,一个投影批里常有 5~8 个节点,其中两条长就命中。
|
|
62
|
+
* 1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值);代价是单条 infer 从 ~2.2s
|
|
63
|
+
* 降到 ~1.2s,反而使 `embedding.ts` 的 EMA>5s 熔断更不容易被误触发。
|
|
64
|
+
* 它同时是**崩溃护栏与内存护栏**:峰值按 O(T²) 增长(attention 张量 `[1,heads,T,T]`)。
|
|
57
65
|
*
|
|
58
66
|
* 不拿 `maxInputChars` 当护栏的原因:bge-m3/embeddinggemma 是 Unigram/BPE,中文≈**1 字/token**
|
|
59
67
|
* 而 emoji/泰文可 >1.5,所以 5000 字符**既可只 750 token(英文)也可近 8192(中文)**——
|
|
60
68
|
* 字符与 token 无稳定比值,字符帽永远约不到安全线。
|
|
61
69
|
*/
|
|
62
|
-
export declare const HARD_CAP_TOKENS =
|
|
70
|
+
export declare const HARD_CAP_TOKENS = 1536;
|
|
63
71
|
/**
|
|
64
|
-
* 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-
|
|
72
|
+
* 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-token-guard.cjs`)。
|
|
65
73
|
*
|
|
66
74
|
* 默认(`userTokens` 缺省/0/负)= `min(modelContext, HARD_CAP_TOKENS)`:bge-small-zh(512)
|
|
67
|
-
* 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192)
|
|
75
|
+
* 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到硬上限。`modelContext`
|
|
68
76
|
* 取不到(非目录模型)时回退 HARD_CAP_TOKENS,**取小不取大**。
|
|
69
77
|
*
|
|
70
78
|
* 用户显式超限只 **clamp 不抛错**(收窄 schema 边界会让已部署 YAML 解析直接失败,故障半径
|
|
@@ -78,21 +78,29 @@ export function catalogById(id) {
|
|
|
78
78
|
/**
|
|
79
79
|
* 本地 ONNX 推理的单条 token 上限(**唯一硬编码源**,issue #22)。
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* 为什么是 1536 而不是 2048:本值受**连发**致死线约束,不是单发线。
|
|
82
|
+
* #22 报告人在 macOS/M5 上二分实测(Electron-node,5×5000 中文字符串行):
|
|
83
|
+
* 512/1024 全过、1900/1960 全过、**1990/2000/2010/2016/2020/2040/2048 均第 2 条即 SIGTRAP**
|
|
84
|
+
* —— 即**连发致死线落在 1960~1990**,而单发致死线是 ≈2700(issue 初测)。
|
|
85
|
+
* 两者不同源的机理:BFCArena 高水位不回落,第二条进来时第一份 T² 张量尚未回收完,
|
|
86
|
+
* 新块与旧块共存瞬间的峰值才是真正的天花板。
|
|
87
|
+
*
|
|
88
|
+
* 而**真实负载就是连发**:`pipeline/graph.ts` 对 touched 节点「`for … await embed(…)`」
|
|
89
|
+
* 逐条串行,一个投影批里常有 5~8 个节点,其中两条长就命中。
|
|
90
|
+
* 1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值);代价是单条 infer 从 ~2.2s
|
|
91
|
+
* 降到 ~1.2s,反而使 `embedding.ts` 的 EMA>5s 熔断更不容易被误触发。
|
|
92
|
+
* 它同时是**崩溃护栏与内存护栏**:峰值按 O(T²) 增长(attention 张量 `[1,heads,T,T]`)。
|
|
85
93
|
*
|
|
86
94
|
* 不拿 `maxInputChars` 当护栏的原因:bge-m3/embeddinggemma 是 Unigram/BPE,中文≈**1 字/token**
|
|
87
95
|
* 而 emoji/泰文可 >1.5,所以 5000 字符**既可只 750 token(英文)也可近 8192(中文)**——
|
|
88
96
|
* 字符与 token 无稳定比值,字符帽永远约不到安全线。
|
|
89
97
|
*/
|
|
90
|
-
export const HARD_CAP_TOKENS =
|
|
98
|
+
export const HARD_CAP_TOKENS = 1536;
|
|
91
99
|
/**
|
|
92
|
-
* 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-
|
|
100
|
+
* 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-token-guard.cjs`)。
|
|
93
101
|
*
|
|
94
102
|
* 默认(`userTokens` 缺省/0/负)= `min(modelContext, HARD_CAP_TOKENS)`:bge-small-zh(512)
|
|
95
|
-
* 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192)
|
|
103
|
+
* 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到硬上限。`modelContext`
|
|
96
104
|
* 取不到(非目录模型)时回退 HARD_CAP_TOKENS,**取小不取大**。
|
|
97
105
|
*
|
|
98
106
|
* 用户显式超限只 **clamp 不抛错**(收窄 schema 边界会让已部署 YAML 解析直接失败,故障半径
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-prime-memory",
|
|
3
3
|
"name": "dsh-prime-memory",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.27",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|