|
| 1 | +# mold — Peer Review |
| 2 | + |
| 3 | +**Reviewer:** Bender |
| 4 | +**Date:** 2026-03-12 |
| 5 | +**Commit:** d508b7a |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Summary |
| 10 | + |
| 11 | +Core inference pipeline is working (GGUF loading, device split, VAE decode). The scaffolding is solid. However there are several critical correctness bugs, zero tests, and a handful of medium-priority issues that need addressing before this is production-ready. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Critical |
| 16 | + |
| 17 | +### 1. `MoldClient::generate` is completely broken |
| 18 | +**File:** `crates/mold-core/src/client.rs` |
| 19 | + |
| 20 | +The client calls `.json::<GenerateResponse>()` to parse the response, but the server returns raw `image/png` bytes with `Content-Type: image/png`. They are fundamentally incompatible. The CLI (`mold generate`) will always fail with a JSON parse error. |
| 21 | + |
| 22 | +**Fix:** `generate()` must return `Vec<u8>` (raw bytes) and the caller reconstructs `GenerateResponse`. |
| 23 | + |
| 24 | +### 2. Panics produce empty 500s with no diagnostics |
| 25 | +**File:** `crates/mold-server/src/routes.rs` |
| 26 | + |
| 27 | +The generate handler uses `map_err(|e| ...)` which only catches `Err`. CUDA OOM and other runtime panics (e.g., VAE decode at 1024×1024) cause tokio to catch the panic and return `500 ""` with no body and no log. Already bit us multiple times. |
| 28 | + |
| 29 | +**Fix:** Wrap the blocking inference call in `tokio::task::spawn_blocking` + `std::panic::catch_unwind` and map panics to proper error responses. Also add `tracing::error!` in the map_err closure. |
| 30 | + |
| 31 | +### 3. Generate handler holds the mutex for the entire inference duration |
| 32 | +**File:** `crates/mold-server/src/routes.rs` |
| 33 | + |
| 34 | +`state.engine.lock().await` is held for the full 8–30s generation. Any concurrent request will queue behind it (tokio::sync::Mutex is fair). This is arguably intentional (GPU is single-threaded), but it means a hung generation blocks all other requests including `/health`. The lock should be released before blocking I/O where possible, and the inference should run on `spawn_blocking`. |
| 35 | + |
| 36 | +### 4. No request validation |
| 37 | +**File:** `crates/mold-core/src/types.rs`, `crates/mold-server/src/routes.rs` |
| 38 | + |
| 39 | +`GenerateRequest` has no bounds checking. A request with `width: 0` or `steps: 0` produces cryptic candle panics instead of a 422. Minimum viable validation: |
| 40 | +- `width` and `height`: must be > 0, must be multiples of 16 (FLUX requirement), and capped at 768 (current OOM limit) |
| 41 | +- `steps`: must be >= 1 |
| 42 | +- `prompt`: must not be empty |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## High Priority |
| 47 | + |
| 48 | +### 5. CLIP prompt not truncated to 77 tokens |
| 49 | +**File:** `crates/mold-inference/src/engine.rs` |
| 50 | + |
| 51 | +CLIP has a hard maximum of 77 tokens. The code passes raw token IDs without truncation. Long prompts will silently produce incorrect output or panic inside CLIP's attention mechanism. |
| 52 | + |
| 53 | +```rust |
| 54 | +// Add before building input_ids: |
| 55 | +tokens.truncate(77); |
| 56 | +``` |
| 57 | + |
| 58 | +### 6. `is_schnell` detected by string match on model name |
| 59 | +**File:** `crates/mold-inference/src/engine.rs` |
| 60 | + |
| 61 | +`self.model_name.contains("schnell")` is fragile. A model named `"my-schnell-dev"` would incorrectly use the schnell schedule; a GGUF named `"flux1-schnell-Q8_0.gguf"` would only match if the model_name also contains "schnell". Should use an explicit enum `ModelFamily::Schnell | ModelFamily::Dev` resolved at load time. |
| 62 | + |
| 63 | +### 7. T5 config hardcoded as raw JSON string |
| 64 | +**File:** `crates/mold-inference/src/engine.rs` |
| 65 | + |
| 66 | +The T5-XXL config is embedded as a 400-char JSON string literal. If candle's `t5::Config` gains or removes fields this silently breaks (or panics on deserialization). Should be a typed `const` or built from a struct literal, not deserialized from a string that can't be checked at compile time. |
| 67 | + |
| 68 | +### 8. `unsafe` mmap with no path existence check |
| 69 | +**File:** `crates/mold-inference/src/engine.rs` |
| 70 | + |
| 71 | +All four `VarBuilder::from_mmaped_safetensors` calls are `unsafe` and produce unreadable errors if the path doesn't exist. Add a simple pre-check: |
| 72 | + |
| 73 | +```rust |
| 74 | +if !path.exists() { |
| 75 | + bail!("model file not found: {}", path.display()); |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +### 9. Default CLI width/height is 1024 but 1024 causes OOM |
| 80 | +**File:** `crates/mold-cli/src/main.rs` |
| 81 | + |
| 82 | +`--width` and `--height` default to 1024 in the CLI, but 1024×1024 panics (VAE OOM) on the current setup. The default should match the safe operating range: **768**. |
| 83 | + |
| 84 | +### 10. `candle` pinned to `branch = "main"` |
| 85 | +**File:** `crates/mold-inference/Cargo.toml` |
| 86 | + |
| 87 | +`branch = "main"` is non-reproducible — a `cargo update` on a different day can silently pull in a breaking candle commit. Pin to a specific `rev = "..."`. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Medium Priority |
| 92 | + |
| 93 | +### 11. Dead error types: `MoldError` and `InferenceError` are unused |
| 94 | +**Files:** `crates/mold-core/src/error.rs`, `crates/mold-inference/src/error.rs` |
| 95 | + |
| 96 | +Both error enums are defined but the actual code uses `anyhow::Error` throughout. Either use them (makes error handling more structured and allows `match` on error variants) or remove them to reduce confusion. |
| 97 | + |
| 98 | +### 12. Dead API client methods hit non-existent endpoints |
| 99 | +**File:** `crates/mold-core/src/client.rs` |
| 100 | + |
| 101 | +`MoldClient::load_model()` and `unload_model()` call `/api/models/load` and `/api/models/{model}` which don't exist in the router. They'll always return 404. Either implement the routes or remove the methods. |
| 102 | + |
| 103 | +### 13. `mold serve` CLI command is a silent stub |
| 104 | +**File:** `crates/mold-cli/src/commands/serve.rs` |
| 105 | + |
| 106 | +Prints a warning and exits with Ok(()). Users who run `mold serve` get no actual server. Should either call `mold_server::run_server()` directly (it's a library crate, this is already possible) or return an explicit error. |
| 107 | + |
| 108 | +### 14. `mold pull` simulates a download |
| 109 | +**File:** `crates/mold-cli/src/commands/pull.rs` |
| 110 | + |
| 111 | +Runs a fake progress bar and exits. Fine as a placeholder, but the fake progress bar is confusing — it implies the model was downloaded when nothing happened. |
| 112 | + |
| 113 | +### 15. Config TOML parse errors swallowed silently |
| 114 | +**File:** `crates/mold-core/src/config.rs` |
| 115 | + |
| 116 | +```rust |
| 117 | +Err(_) => Config::default(), // silently ignores parse error |
| 118 | +``` |
| 119 | + |
| 120 | +Should `tracing::warn!` or at minimum `eprintln!` so users know their config is being ignored. |
| 121 | + |
| 122 | +### 16. `hf-hub` and `uuid` are unused dependencies |
| 123 | +**Files:** `crates/mold-inference/Cargo.toml`, `crates/mold-core/Cargo.toml` |
| 124 | + |
| 125 | +`hf-hub` is pulled into mold-inference but never called (no download code exists). `uuid` is in mold-core but not used in any visible code. Both increase compile time and binary size for no benefit. |
| 126 | + |
| 127 | +### 17. GPU info always null |
| 128 | +**File:** `crates/mold-server/src/routes.rs` |
| 129 | + |
| 130 | +`ServerStatus.gpu_info` is always `None`. `nvidia-smi` or candle's device API can provide this. Low priority but `mold ps` showing "GPU: not detected" is misleading. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Minor / Style |
| 135 | + |
| 136 | +### 18. Redundant `.map_err(anyhow::Error::from)` in `FluxTransformer::denoise` |
| 137 | +`flux::sampling::denoise` returns `candle_core::Result<Tensor>` which auto-converts via `?`. The explicit `.map_err(anyhow::Error::from)` is unnecessary. |
| 138 | + |
| 139 | +### 19. `t5_emb.clone()` in the non-quantized path wastes a copy |
| 140 | +In the non-quantized branch: `(t5_emb.clone(), clip_emb.clone(), img.clone())` — the originals are never used again, so move instead of clone. |
| 141 | + |
| 142 | +### 20. `LoadedFlux.cpu` device stored unnecessarily |
| 143 | +`Device::Cpu` is a zero-cost value (`Device::Cpu` is a unit variant). Storing it in `LoadedFlux` adds no value — just use `Device::Cpu` inline during generate. |
| 144 | + |
| 145 | +### 21. `model_registry::find_model` is dead code |
| 146 | + |
| 147 | +### 22. `LoadModelRequest` in types.rs is orphaned |
| 148 | +Only used by dead client methods. |
| 149 | + |
| 150 | +### 23. No README.md at repo root |
| 151 | +CLAUDE.md exists but GitHub shows no README on the repo landing page. |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Test Coverage: Zero |
| 156 | + |
| 157 | +No tests exist anywhere. Minimum required: |
| 158 | + |
| 159 | +| What | Where | Priority | |
| 160 | +|------|-------|----------| |
| 161 | +| `OutputFormat` parsing (png, jpeg, jpg, invalid) | `mold-core` | High | |
| 162 | +| `GenerateRequest` validation bounds | `mold-core` | High | |
| 163 | +| `Config::load_or_default` with missing/corrupt file | `mold-core` | High | |
| 164 | +| `ModelPaths::resolve` env var precedence over config | `mold-core` | Medium | |
| 165 | +| API route unit tests (mock engine, check HTTP codes) | `mold-server` | High | |
| 166 | +| `model_registry::known_models` returns expected entries | `mold-inference` | Low | |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## Fixes Implemented |
| 171 | + |
| 172 | +See commits following this review. |
0 commit comments