breadsearch/docs/error-reports/2026-06-24-oom-minified-json-chunking.md
2026-07-02 20:59:18 +08:00

238 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Error Report: breadmill OOM loop on minified JSON `.txt` file
**Date:** 2026-06-24
**Reporter:** breadway
**Component:** `breadmill` (semantic search indexer)
**Version:** 0.1.0 (`/home/breadway/Projects/breadsearch/breadmill`)
**Severity:** Critical — repeated kernel OOM kills, ~28 GB RAM consumption, system instability
---
## Summary
`breadmill` enters a crash loop when indexing a 705 KB minified JSON game save file exposed as `.txt`. Word-based chunking produces ~486616 KB chunks that are passed unbounded to the ONNX embedding model. ONNX Runtime then attempts multi-terabyte allocations in attention `MatMul`, the process balloons to ~28 GB RSS, and the kernel OOM killer terminates it. `Restart=on-failure` causes an immediate restart and the cycle repeats.
On 2026-06-24 this produced **40 embed errors** and **76 OOM kills** in journal logs within ~45 minutes.
---
## Environment
| Item | Value |
|------|-------|
| Host | Lenovo Yoga Slim 7 14AKP10 (`83JY`) |
| OS | BOS (Arch-based), kernel `7.0.12-arch1-1` |
| RAM | 32 GB |
| Swap | 4 GB zram |
| Model | `nomic-embed-text-v1.5` (768-dim, ONNX via `ort` 2.0.0-rc.12) |
| Service | `breadmill.service` (user systemd unit) |
| Config | `~/.config/breadsearch/config.toml` |
### Service unit
```ini
[Service]
Type=simple
ExecStart=%h/.cargo/bin/breadmill
Restart=on-failure
RestartSec=5
# No MemoryMax set (unlimited)
# OOMScoreAdjust=200 (default for user services — preferential OOM victim)
```
### Relevant config
```toml
[index]
roots = ["~/Documents", "~/.config/breadpad"]
extensions = ["md", "txt", "org", "pdf", "odt", "docx"]
max_file_mb = 5.0
```
---
## Triggering file
**Path:**
`~/Documents/Gaming/Games/Spaceflight Simulator Game/Saving/Worlds/Bread/Persistent/Rockets.txt`
| Property | Value |
|----------|-------|
| Size | 721,601 bytes (705 KB) |
| Type | JSON array (11 rocket objects), stored as `.txt` |
| Lines | **1** (single-line minified JSON) |
| Whitespace-delimited "words" | 601 |
| Longest "word" | 68,068 characters |
The file is under `max_file_mb = 5.0` and has extension `.txt`, so it is indexed.
---
## Root cause
### 1. Word-based chunking breaks on minified JSON
`chunk::chunk_text()` splits on whitespace only (`chunk.rs`). For minified JSON, most structure is packed into very long tokens with few spaces.
Chunking with `words_per_chunk=400`, `overlap_words=80` (`indexer.rs:268`) yields:
| Chunk | Word range | Character length |
|-------|------------|------------------|
| 0 | 0399 | **486,347** |
| 1 | 320600 | **616,582** |
A "400-word chunk" is not ~400 natural-language words; it is hundreds of kilobytes of dense JSON.
### 2. No token/character limit before embedding
`embed::OrtEmbedder::embed_with_prefix()` tokenizes the full chunk and runs ONNX inference with no `max_length` truncation (`embed.rs`). The nomic model supports ~8192 tokens; these chunks are orders of magnitude larger when tokenized.
### 3. ONNX attention allocation explodes
Embedding fails inside ONNX Runtime:
```
Non-zero status code returned while running FusedMatMul node.
Name: '/encoder/layers.0/attn/MatMul/MatMulScaleFusion/'
Status Message: ... BFCArena::AllocateRawInternal ...
Failed to allocate memory for requested buffer of size 5525780084992
```
Requested buffer: **~5.1 TiB** (5,525,780,084,992 bytes).
Kernel also logs repeated allocation attempts before OOM:
```
__vm_enough_memory: pid: NNNN, comm: breadmill,
bytes: 8796093026304 not enough memory for the allocation (~8.0 TiB)
bytes: 7916483514368 not enough memory for the allocation (~7.2 TiB)
bytes: 7124834848768 not enough memory for the allocation (~6.5 TiB)
```
### 4. Restart loop amplifies damage
`Restart=on-failure` + `RestartSec=5` restarts breadmill immediately after each OOM kill. Each restart reloads the model, rescans `~/Documents`, hits the same file, and OOMs again. Peak memory per attempt: **~2728 GB RSS** (`anon-rss:27867432kB`).
---
## Log excerpts
### Embed error (repeats on every restart)
```
breadmill: embed error for .../Rockets.txt: Non-zero status code returned while running FusedMatMul node.
Name:'/encoder/layers.0/attn/MatMul/MatMulScaleFusion/' Status Message:
... Failed to allocate memory for requested buffer of size 5525780084992
```
### OOM kill
```
oom-kill: ... task_memcg=.../breadmill.service, task=breadmill, pid=4515, uid=1000
Out of memory: Killed process 4515 (breadmill)
total-vm:64081848kB, anon-rss:27867432kB, ... oom_score_adj:200
systemd[1649]: breadmill.service: Failed with result 'oom-kill'.
systemd[1649]: breadmill.service: Consumed ... 27.9G memory peak, 2.1G memory swap peak.
systemd[1649]: breadmill.service: Scheduled restart job, restart counter is at 8.
```
### Secondary noise (non-fatal)
```
breadmill: extract .../Novacana Info.txt: stream did not contain valid UTF-8
```
---
## Reproduction
1. Place a single-line minified JSON file (>400 KB, `.txt` extension) under a configured index root.
2. Start `breadmill serve` (or `systemctl --user start breadmill.service`).
3. Wait for initial scan to reach the file.
4. Observe embed errors, climbing RSS, and OOM kills in `journalctl --user -u breadmill.service -f`.
**Minimal reproducer characteristics:**
- Extension in `extensions` list (e.g. `txt`)
- File size `< max_file_mb`
- Content is minified JSON or other whitespace-sparse text on one line
- Results in chunks >> model `max_seq_len` when tokenized
---
## Impact
- **breadmill** unusable while the file is present in index roots
- **System-wide** memory pressure: swap exhaustion, unrelated process kills, journal/cache pressure
- Can be mistaken for unrelated storage or suspend issues when swap write errors appear in kernel log
---
## Suggested fixes
### Required (correctness)
1. **Truncate before tokenization** — cap input to model `max_seq_len` (8192 tokens for nomic-embed-text-v1.5) in `embed.rs`, with explicit logging when truncation occurs.
2. **Character-based chunk limits** — add `max_chunk_chars` (e.g. 8_00032_000) independent of word count; split oversized chunks before embedding.
3. **Sanity-check chunk size** — refuse to embed chunks above a byte/token threshold; log and skip rather than calling ONNX.
### Recommended (resilience)
4. **Per-chunk error isolation** — on embed failure for one chunk, skip that chunk but do not retry the entire file in a tight loop; mark file as `failed` in SQLite.
5. **Service memory limit** — set `MemoryMax=4G` (or similar) on `breadmill.service` so a runaway embed cannot take the whole machine.
6. **Lower `OOMScoreAdjust`** — use `0` or negative so breadmill is not preferentially killed while still allowing limits.
7. **Backoff on repeated OOM**`RestartSec=exponential` or stop after N OOM kills per file.
### Optional (UX)
8. **Exclude patterns** — config option for glob excludes (e.g. `**/Saving/**`, `**/*.json` even if renamed `.txt`).
9. **Detect minified JSON** — if `serde_json::from_str` succeeds on `.txt`, skip or pretty-print/chunk differently.
10. **`--version` flag** — aids bug reports (currently `unknown command: --version`).
---
## Workaround (immediate)
Exclude the game save directory from index roots in `~/.config/breadsearch/config.toml`:
```toml
[index]
roots = [
"~/Documents",
"~/.config/breadpad",
]
# Then add an exclude mechanism when available, OR temporarily narrow roots:
# roots = ["~/Documents/Creative", "~/.config/breadpad"]
```
Or stop the service until a fix is deployed:
```bash
systemctl --user stop breadmill.service
```
---
## Files involved
| File | Role |
|------|------|
| `breadmill/src/chunk.rs` | Whitespace word chunking — no char/token cap |
| `breadmill/src/indexer.rs:268` | `chunk_text(&text, 400, 80)` — hardcoded params |
| `breadmill/src/embed.rs` | No truncation before `tokenizer.encode()` / ONNX run |
| `breadmill/src/extract.rs` | Treats `.txt` as raw UTF-8 (JSON passes through) |
| `~/.config/breadsearch/config.toml` | `max_file_mb` only; no chunk/token limits |
---
## Related
- ONNX node: `/encoder/layers.0/attn/MatMul/MatMulScaleFusion/`
- Model: [nomic-embed-text-v1.5](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5) (max sequence length 8192)