fix the deadlock at server startup#2443
Conversation
Symptom: lemond starts, prints something about "Building models cache", prints some other stuff, but never says it "Cache built". A handful of debug prints show that there's a path calling get_system_info_with_cache that recursively calls get_system_info_with_cache ... which tries to get a mutex on s_system_info_mutex. "Well there's your problem!" I'm also not sure why I'm only just running into this now. Co-authored-by: opencode, Qwen3.6-35B-A3B-MTP
|
@claude review |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
In-Depth Review — Found Critical Bug + Several Hardening Issues
Thanks for this fix! The overall approach (overload pattern + lock scope reduction + recursion guard + sysfs bypass) is sound. However, I found several issues during a thorough review, including one critical bug that prevents recipe caching from ever working.
🔴 Critical: is_outermost_call always false — recipes never cached
In get_system_info_with_cache(), the original code determines the outermost caller with:
bool is_outermost_call = !s_phase2_in_progress.exchange(false);exchange(false) returns the old value. After the earlier exchange(true) set it to true, this returns true, so !true = false. The outermost caller never writes recipes to cache, s_recipes_computed stays false forever, and Phase 2 recomputes on every single call.
Fix: The variable was_already_in_progress already captures the information needed — it is false iff this thread set the flag from false→true. Use that directly. Also restructured to hoist the declaration outside the try block so it is in scope after the catch, and only the outermost caller resets the flag.
🟠 Moderate: detect_rocm_arch_from_sysfs() Priority 2 is dead code
The PCI device ID → gfx target mapping loop reads vendor and device IDs from sysfs, but the mapping table between device IDs and gfx architecture strings was entirely missing — just a comment where the code should have been. On systems without /sys/class/kfd/kfd/topology/nodes, the fallback silently returns "".
Fix: Added the full mapping table covering RDNA2 (Navi 21/22/23/24), RDNA3 (Navi 31/32/33), RDNA3.5 iGPU (Strix Point, Strix Halo, Krackan Point), and RDNA4 (Navi 48).
Additional Hardening (6 more fixes)
| Issue | Fix |
|---|---|
KFD std::stoi failure returns "" immediately instead of falling through to Priority 2 |
Changed to continue — tries next KFD node, then falls through to PCI mapping |
return s_cached_system_info without lock — data race with concurrent Phase 2 writer installing ["recipes"] |
Wrapped return in lock_guard<s_system_info_mutex> to copy under lock |
invalidate_recipes() races with in-progress Phase 2 — invalidation clears s_recipes_computed, but Phase 2 finishes and sets it back to true, caching stale recipes |
Added std::atomic<uint64_t> s_recipe_generation counter; Phase 2 snapshots before computing and re-checks under lock |
Failed computation caches empty recipes — if create_system_info() or build_recipes_info() throws, empty recipes were cached and s_recipes_computed set to true, preventing retry |
Added computation_ok flag; failures leave s_recipes_computed = false |
Stale lock-ordering comments in headers and cpp referencing old models_cache_mutex_ → s_system_info_mutex ordering |
Updated to describe current ordering |
| PCI_GFX_MAP key case mismatch — sysfs outputs lowercase hex | Map keys are now lowercase |
Lock Ordering Verified
Checked all 30+ models_cache_mutex_ acquisition sites — zero violations of s_system_info_mutex → models_cache_mutex_.
Edge Cases Traced
Single-threaded startup, two-thread race into build_cache(), recursive call during Phase 2, concurrent invalidate_recipes() during computation, exception during computation, nested caller returning while outermost writes, unparseable KFD data, system without KFD topology nodes, system without AMD GPU — all handled correctly.
Fixes available at
bong-water-water-bong/1bit-lemonade:fix/deadlock-review-fixes (commit f230620a)
|
thanks. I'll have a look at that tomorrow |
|
@ckuethe I completed an in-depth review of this PR and found several issues including one critical bug where All fixes are on git pull https://github.com/bong-water-water-bong/1bit-lemonade.git pr-2443The branch compiles cleanly ( 🤖 Generated with Claude Code |
|
@ckuethe Updated the fix branch with additional hardening from a full 4-agent review. Here is the complete summary: What was found and fixed (14 issues)Critical
High
Medium
Hardening
Test results
Pullgit pull https://github.com/bong-water-water-bong/1bit-lemonade.git pr-2443🤖 Generated with Claude Code |
|
@ckuethe GitHub CLI does not support cross-fork PR creation, but you can create the PR with one click here: ckuethe/lemonade@startup_deadlock...bong-water-water-bong:pr-2443 That link opens a PR from |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Found a critical bug in the is_outermost_call logic:
system_info.cpp: bool is_outermost_call = !s_phase2_in_progress.exchange(false);
exchange() returns the PREVIOUS value. For the outermost caller, the previous value is true (set by the earlier exchange(true)), so !true = false. This means is_outermost_call is always false — recipes are never written to cache, and s_recipes_computed is never set to true. This causes:
- FLM models never discovered (get_flm_status returns unsupported)
- All recipe checks fail (no recipes in system_info)
- filter_models_by_backend filters out all models
- Phase 2 recomputes on every single invocation (redundant I/O)
Fix (one line): The existing was_already_in_progress variable already correctly identifies the outermost caller. Replace:
bool is_outermost_call = !s_phase2_in_progress.exchange(false);with:
bool is_outermost_call = !was_already_in_progress;
s_phase2_in_progress.store(false);s_phase2_in_progress.exchange(false) returns the PREVIOUS value (true for the outermost caller), so !true = false. This meant is_outermost_call was ALWAYS false and recipes were never written to cache. Fix: use the already-saved was_already_in_progress variable to determine if this is the outermost caller, then reset the atomic with store(false). This resolves a critical bug where recipes (FLM status, GPU detection, install params) were never persisted across API calls, causing redundant subprocess and network I/O on every get_system_info_with_cache() call. Co-Authored-By: Claude <noreply@anthropic.com>
|
pulled your bong-water-water-bong@a7fb81e into this PR |
- Fix KFD stoi parse failure: use continue instead of return "" so the loop tries the next KFD topology node before giving up - Add PCI device ID → gfx mapping table in detect_rocm_arch_from_sysfs() covering RDNA2 (Navi 21-24), RDNA3 (Navi 31-33), RDNA4 (Navi 44/48); also add missing #include <unordered_map> and drop the dead minor_str variable - Fix was_already_in_progress initialization: move exchange() call before the try block so the flag is always properly owned and reset - Fix failed computation caches empty recipes: add computation_ok flag so s_recipes_computed is only set to true on success, allowing retries - Fix invalidate_recipes() race with in-progress Phase 2: add s_recipe_generation atomic counter; Phase 2 snapshots the generation before computing and only writes to cache if the generation still matches - Fix final return data race: return s_cached_system_info under lock
- Fix KFD stoi parse failure: use continue instead of return "" so the loop tries the next KFD topology node before giving up - Add PCI device ID → gfx mapping table in detect_rocm_arch_from_sysfs() covering RDNA2 (Navi 21-24), RDNA3 (Navi 31-33), RDNA4 (Navi 44/48); also add missing #include <unordered_map> and drop the dead minor_str variable - Fix was_already_in_progress initialization: move exchange() call before the try block so the flag is always properly owned and reset - Fix failed computation caches empty recipes: add computation_ok flag so s_recipes_computed is only set to true on success, allowing retries - Fix invalidate_recipes() race with in-progress Phase 2: add s_recipe_generation atomic counter; Phase 2 snapshots the generation before computing and only writes to cache if the generation still matches - Fix final return data race: return s_cached_system_info under lock
|
Thanks for the feedback @bong-water-water-bong |
|
Let's evaluate #2488 as a potentially simpler alternative. |
|
#2488 works too, and is a smaller, simpler diff than this one. |
…l alt to lemonade-sdk#2443) (lemonade-sdk#2488) * Fix system-info startup self-deadlock with a thread-local recursion guard get_system_info_with_cache() holds s_system_info_mutex across the whole recipe build. build_recipes_info() can re-enter the function on the same thread (build_recipes_info -> backend install-params -> get_rocm_arch -> get_system_info_with_cache), and re-locking the non-recursive mutex self-deadlocks, hanging startup at "Building models cache...". Fixes lemonade-sdk#2414. Break the recursion with a thread_local flag: the re-entrant call returns the already-computed hardware snapshot (all get_rocm_arch needs) instead of re-locking. thread_local is intentional — only the recursing thread short-circuits, so concurrent threads still block on the mutex and get fully-populated data rather than a recipe-less snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document hardware-computed invariant on the recursion-guard early return Per review: the s_building_recipes flag is only set after hardware detection, so the early-return snapshot always has "devices" populated. Make that ordering dependency explicit in the comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symptom: lemond starts, prints something about "Building models cache", prints some other stuff, but never says it "Cache built". A handful of debug prints show that there's a path calling get_system_info_with_cache that recursively calls get_system_info_with_cache ... which tries to get a mutex on s_system_info_mutex. "Well there's your problem!"
I'm also not sure why I'm only just running into this now, but it fixes #2414
Co-authored-by: opencode, Qwen3.6-35B-A3B-MTP