```
feat(iOS): 更新MNN后端模型配置优化性能 将MNN主模型从Qwen3.5-4B(~2.64GiB)降级为Qwen3.5-2B(~1.1GiB),因为4B版本 实测运行过慢,影响用户体验。iPhone17+/SME2设备使用2B模型,保留MLX 兜底方案用于模拟器和备用场景,确保AI推理性能和存储效率的平衡。 ```
This commit is contained in:
210
docs/踩坑与排查记录.md
Normal file
210
docs/踩坑与排查记录.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 康康 · 踩坑与排查记录
|
||||
|
||||
> 本地推理 / SwiftData / 端侧模型这类问题不好复现也不好搜,踩过的坑按统一模板记在这里,方便回查。
|
||||
> 新增条目往最上面加(倒序),模板见文末。
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-09 · 生成身体档案报告时,LLM 逐行复读死循环
|
||||
|
||||
### 现象
|
||||
多轮「身体档案」对话点生成报告后,「## 关键指标」整段陷入死循环:同一行
|
||||
`⚠️ 收缩压 (107 mmHg)` 连续重复几十遍,最后被 maxTokens 截断成半行「⚠️ 收缩」。
|
||||
(本质是小模型 **repetition / degeneration loop**,不是数据真有几十条。)
|
||||
|
||||
### 根因(确认)
|
||||
采样器**完全没有重复惩罚**,叠加低温 → 几乎必然复读。两个后端都有问题:
|
||||
|
||||
| 后端 | 位置 | 原配置 | 问题 |
|
||||
|---|---|---|---|
|
||||
| MNN(主) | `MNNLLMBridge.mm` `initWithConfigPath` 的 `set_config` | `temperature 0.3, topP 0.85` | 无 `penalty` |
|
||||
| MLX(兜底) | `LLMSession.swift` `GenerateParameters` | `temperature 0.3, topP 0.85` | 无 `repetitionPenalty` |
|
||||
|
||||
关键细节(读 MNN 源码 `transformers/llm/engine/src/`):
|
||||
- `llmconfig.hpp`:`mixed_samplers` 默认 `{topK, tfs, typical, topP, min_p, temperature}` —— **不含 `penalty`**;
|
||||
`penalty` / `ngram_factor` 默认 `1.0`(=全关)。
|
||||
- `sampler.cpp` `configMixed`:只会把 `penalty`「**移到链首(如果存在)**」,**不会自动插入**。
|
||||
所以光设 `"penalty":1.1` 没用,必须把 `"penalty"` 显式写进 `mixed_samplers`。
|
||||
- `sampler.cpp` `stepPenalty`:`repetition_penalty` 对 logits 乘法惩罚;**n-gram 命中整段重复时惩罚直接升到 `max_penalty`** —— 这正是掐断「整行复读」最有效的开关。
|
||||
|
||||
**为什么低温反而更糟**:temperature 0.3 接近贪心,一旦吐出 `收缩压 (107 mmHg)\n`,
|
||||
最高概率的后续就是再吐一遍同样的行,无惩罚就永远出不来。
|
||||
|
||||
### 排查过程(可复用思路)
|
||||
1. 看现象先判定是「数据重复」还是「生成复读」—— 被截断成半行 `收缩` 说明是 token 级复读,不是数据。
|
||||
2. `grep -niE "penalty|temperature|top_?p|sampler" 康康/AI/` 一把定位两个后端的采样配置 → 都没 penalty。
|
||||
3. 不猜 MNN 配置键,直接读构建用的源码 `MNN_SRC=/Users/xuhuayong/apps/MNN-src`
|
||||
的 `llmconfig.hpp` / `sampler.cpp`,确认键名、默认值、`mixed_samplers` 不自动插 penalty。
|
||||
4. MLX 侧读 SPM checkout 的 `MLXLMCommon/Evaluate.swift`,确认 `GenerateParameters` 有
|
||||
`repetitionPenalty: Float?` + `repetitionContextSize: Int`。
|
||||
|
||||
### 修复
|
||||
- **MNN** `MNNLLMBridge.mm`:`set_config` 显式开重复惩罚 +
|
||||
把 `penalty` 放进 mixed 链首:
|
||||
```jsonc
|
||||
{
|
||||
"jinja":{"context":{"enable_thinking":false}},
|
||||
"sampler_type":"mixed",
|
||||
"mixed_samplers":["penalty","topK","topP","temperature"],
|
||||
"temperature":0.3,"topP":0.85,"topK":40,
|
||||
"penalty":1.1,"n_gram":8,"ngram_factor":1.05
|
||||
}
|
||||
```
|
||||
(注意:JSON merge-patch 对数组是**整体替换**,所以这里会覆盖掉默认 `mixed_samplers`,符合预期。)
|
||||
- **MLX** `LLMSession.swift`:`GenerateParameters(..., repetitionPenalty: 1.1, repetitionContextSize: 64)`。
|
||||
|
||||
取值都偏保守:`penalty 1.1` / `ngram_factor 1.05` 是业界常用档(MNN 自带 omni 默认 1.05),
|
||||
低温 + 轻惩罚既能掐复读,又不破坏 JSON / 结构化输出的稳定性。
|
||||
|
||||
### 验证
|
||||
- `xcodebuild ... -destination generic/platform=iOS` 编译通过(两个后端均编进)。
|
||||
- ⚠️ **真机/模拟器跑一遍多轮导出生成报告**,确认不再复读 —— 复读属推理期行为,单测覆盖不到,必须实跑。
|
||||
|
||||
### 预防 / 相关注意
|
||||
- 任何新增的「长文本生成」(非 JSON 抽取)都走同一套带惩罚的采样参数,别再裸 temperature。
|
||||
- **相关隐患(未修,留观)**:`HealthExportService.retrieveDialogueSnapshot` 取指标时
|
||||
**没有 `prefix` 截断**(窗口检索版 `retrieve` 截了 `prefix(20)`)。指标极多时 prompt 会膨胀、
|
||||
也更易诱发复读。若复发,优先给 dialogue snapshot 也加上限。
|
||||
|
||||
---
|
||||
|
||||
> 以下几条据 W1–W2(2026-05~06)记忆补记,细节以代码/提交为准。
|
||||
|
||||
## 2026-06-09 · MNN 路径 Qwen3.5 强制思考,只吐 `<think>` / JSON 解析失败
|
||||
|
||||
### 现象
|
||||
MNN 真机路径上模型自检只显示 `<think>` 思考过程,AI 辅助拿不到 JSON(解析失败);
|
||||
同样的 prompt 走 MLX 兜底却正常。
|
||||
|
||||
### 根因
|
||||
模型自带 `config.json`(taobao-mnn 预转换件)写死 `"jinja":{"context":{"enable_thinking":true}}`,
|
||||
Qwen3.5 聊天模板据此每个 assistant 回合硬塞 `<think>\n` 开思考,吞掉 token 预算。
|
||||
**prompt 里的 `/no_think` 对 MNN 无效** —— 模板只读 `enable_thinking`,不看文本软开关。
|
||||
只在真机爆是因为 MLX 经 swift-transformers 套模板时不传 `enable_thinking` → 走 else 空 think 块,天然不思考。
|
||||
(这点从仓库代码看不出来,config.json 是下载/旁路导入的模型产物,不在 git 里。)
|
||||
|
||||
### 修复
|
||||
`MNNLLMBridge.mm` 在 `createLLM` 后、`load()` 前 merge-patch 关闭:
|
||||
`set_config("{\"jinja\":{\"context\":{\"enable_thinking\":false}}}")`。不改模型文件、不动字节校验。`stripThink` 保留兜底。
|
||||
|
||||
### 预防
|
||||
再遇 MNN 只出思考 / JSON 解析失败,先查 `config.json` 的 `enable_thinking`,别去调 `/no_think` 或加大预算。
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-07 · 「记录指标·拍照识别」VL 直读化验单不稳 → 改 Vision OCR + LLM
|
||||
|
||||
### 现象
|
||||
Qwen-VL 直读密集小字化验单经常返回 `{"indicators":[]}`(读不出指标)。
|
||||
|
||||
### 根因 / 决策
|
||||
小模型 VL 对密集中文小字不稳。改链路:`DocumentScanner 整页扫描 → Apple Vision OCR(zh-Hans/Hant/en)
|
||||
→ Qwen3 LLM 解析(VLPrompts.indicatorsFromText)→ stripThink → parseIndicatorsJSON → 确认页人工校对 → 存`。
|
||||
Vision OCR 是系统框架、100% 本地,不违反隐私红线。
|
||||
|
||||
### 预防
|
||||
这条路**不要改回 VL 直读**。VL 仍只用于「体检报告归档」整份解读,两者分开。OCR 行分组偶有错位,靠确认页人工校正兜底。
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-01 · git 全量 push 撞 HTTP 413(历史里有 165MB 构建产物)
|
||||
|
||||
### 现象
|
||||
`git push` 到 myv0(Gitea 反代有上传体积限制)报 **HTTP 413**。
|
||||
|
||||
### 根因
|
||||
旧 commit 误把 `build/` 构建产物提交进库(最大单文件 xcarchive DWARF **165MB**),后来虽 `git rm --cached` + `.gitignore`,
|
||||
但对象仍留在历史 → `.git` 87MB,全量 push 超反代上限。
|
||||
|
||||
### 修复
|
||||
对主仓库 `git filter-repo --path build/ --invert-paths --force` 从全历史剥离 → `.git` 87M→2.9M,不再 413。
|
||||
注意:① 重写了所有 commit hash(内容不变),旧克隆需重新 clone;② filter-repo 会移除所有 remote,事后须重新 `git remote add origin`;③ 凭证不写入 `.git/config`。
|
||||
|
||||
### 预防
|
||||
`build/` 必须在 `.gitignore`;别把构建产物 / 大二进制提交进库。
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-31 · 快拍 VL 识别时 App 自动退出(jetsam OOM,非崩溃)
|
||||
|
||||
### 现象
|
||||
iPhone 15 Pro Max 上 VL 识别时 App 直接退出。
|
||||
|
||||
### 根因
|
||||
不是代码崩溃(catch 只切 warning 屏,Swift 报错不会杀进程),是 **OS 内存超限 jetsam kill**。三因叠加:
|
||||
① 无 entitlement(8GB 设备默认单 App 上限 ~3GB,VL ~3GB 常驻冲过);② 从不卸载模型(LLM ~1GB + VL ~3GB 同驻 → 4GB+);③ 没设 MLX cache 上限。
|
||||
|
||||
### 修复
|
||||
① 新建 `康康.entitlements` 加 `com.apple.developer.kernel.increased-memory-limit=true`;
|
||||
② `AIRuntime` 加 `unloadLLM/unloadVL` 做**常驻互斥**(两大模型永不同时驻留)+ actor 内**串行推理闸门**(GPU 同一时刻只一个解码/加载);
|
||||
③ `GPU.set(cacheLimit: 256MB)`,启动调一次。
|
||||
|
||||
### 验证
|
||||
编译 + 单测通过。⚠️ **真机 OOM 是否真消失仍需 iPhone 15 Pro Max 实测**(本机无法跑真机)。
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-30 · 每次重打包 SwiftData 数据被清空
|
||||
|
||||
### 现象
|
||||
W2 期每次重新打包安装,本地数据全没了。
|
||||
|
||||
### 根因
|
||||
`KangkangApp.swift` 里 `ModelContainer` 创建失败的 catch 块原本**直接删 store 文件**。
|
||||
SwiftData 只对纯增量改动自动轻量迁移;一旦 schema 改动超纲(最常见:**给已存在 `@Model` 新增「非可选且无内联默认值」属性**)→ 迁移抛错 → 进 catch → 删库。
|
||||
|
||||
### 修复
|
||||
catch 改为把旧 store(含 `-wal`/`-shm`)挪到 `Application Support/StoreBackups/<时间戳>/` 再重建,不删除。
|
||||
|
||||
### 预防
|
||||
给已存在 `@Model` 加属性**一律给可选或内联默认值**(如 `var x: String = "daily"`),才走轻量迁移。正式发布前升级为 `VersionedSchema` + `SchemaMigrationPlan`。
|
||||
|
||||
---
|
||||
|
||||
## (无明确日期)· 编辑 Localizable.xcstrings 炸出上万行噪声 diff
|
||||
|
||||
### 现象
|
||||
改 `Localizable.xcstrings` 新增 3 个 key,却产生 ~15000 行 diff。
|
||||
|
||||
### 根因
|
||||
仓库里该文件是 **Xcode 规范格式**(`"key" : {` 冒号两侧带空格、2 空格缩进、key 按 Xcode 排序、结尾无换行);
|
||||
用 `python json.dump(indent=2)` 重写会把分隔符变成 `": "` 且顺序不同 → 几乎每行都 diff。
|
||||
|
||||
### 修复 / 正确做法
|
||||
基于 HEAD 原始文本做**文本插入**:把新 key 块按 Xcode 格式(` "<key>" : ` + `separators=(',', ' : ')` 的 value)拼到 strings 段末尾,保持结尾无换行。**不要整文件 json.dump 回写**。
|
||||
|
||||
---
|
||||
|
||||
## 附:命令行编译方式(排查时拿真实错误/警告)
|
||||
|
||||
- 系统默认是 Command Line Tools,裸 `xcodebuild` 不可用,需显式指向完整 Xcode:
|
||||
`export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer`
|
||||
- **必须用独立 derivedDataPath**(如 `-derivedDataPath /tmp/kk-derived-xxx`),否则和 Xcode 抢同一把 `build.db` 锁报 `database is locked`(不是代码错)。
|
||||
- 增量编译会吞警告:要看某文件警告先 `touch` 它强制重编,再 grep `error:|warning:|BUILD (SUCCEEDED|FAILED)`。
|
||||
- 工程是 Swift 5 + `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`;跨到 `nonisolated` 调 MainActor 成员的隔离警告(标 "error in Swift 6 mode")在 Swift 5 下不阻塞构建。
|
||||
|
||||
---
|
||||
|
||||
## 模板(复制下面这段新增条目)
|
||||
|
||||
```markdown
|
||||
## YYYY-MM-DD · 一句话标题
|
||||
|
||||
### 现象
|
||||
(用户看到什么 / 怎么触发)
|
||||
|
||||
### 根因(确认)
|
||||
(定位到的真正原因,不是猜测;贴关键文件:行)
|
||||
|
||||
### 排查过程
|
||||
(怎么一步步定位的,方便下次复用思路)
|
||||
|
||||
### 修复
|
||||
(改了什么,贴 diff 要点或配置)
|
||||
|
||||
### 验证
|
||||
(怎么确认修好了;不能单测的要写明需实跑)
|
||||
|
||||
### 预防 / 相关注意
|
||||
(怎么避免再犯;顺带发现的隐患)
|
||||
```
|
||||
Reference in New Issue
Block a user