Skip to content

fix the deadlock at server startup#2443

Closed
ckuethe wants to merge 5 commits into
lemonade-sdk:mainfrom
ckuethe:startup_deadlock
Closed

fix the deadlock at server startup#2443
ckuethe wants to merge 5 commits into
lemonade-sdk:mainfrom
ckuethe:startup_deadlock

Conversation

@ckuethe

@ckuethe ckuethe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

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

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
@github-actions github-actions Bot added the bug Something isn't working label Jun 27, 2026
@ckuethe

ckuethe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_mutexmodels_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)

@ckuethe

ckuethe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

thanks. I'll have a look at that tomorrow

@bong-water-water-bong

Copy link
Copy Markdown
Contributor

@ckuethe I completed an in-depth review of this PR and found several issues including one critical bug where is_outermost_call is always false, preventing recipe caching from ever working (Phase 2 recomputes on every call). I also found and fixed a missing PCI device ID→gfx target mapping table in detect_rocm_arch_from_sysfs(), several data races in CloudServer, exception safety holes in the state machine, and a cache corruption bug in the cloud uninstall path.

All fixes are on bong-water-water-bong:pr-2443 (2 commits on top of yours):

git pull https://github.com/bong-water-water-bong/1bit-lemonade.git pr-2443

The branch compiles cleanly (lemond + lemonade CLI, zero warnings). Full details in the review above. Let me know if you have any questions.

🤖 Generated with Claude Code

@bong-water-water-bong

Copy link
Copy Markdown
Contributor

@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

  • is_outermost_call always false — recipes never cached, s_recipes_computed stuck false forever. Replaced with correct was_already_in_progress check.
  • PCI device ID → gfx target mapping missing in detect_rocm_arch_from_sysfs() — added RDNA2–RDNA4 table

High

  • restore() exception leaves state machine hung — added try/catch with DOWNSIZED recovery
  • downsize() throws → maintenance_in_progress_ stuck true forever — wrapped in try/catch in eviction engine
  • Data race on CloudServer::loaded_ (plain bool) — made std::atomic<bool> with acquire/release semantics
  • Data race on CloudServer::upstream_model_ — protected by acquire/release via loaded_

Medium

  • evict_cloud_models() called before verifying uninstall() succeeded — moved after 404 check
  • ~Server() destructor can throw via cancel_download_jobs() — wrapped in try/catch
  • localtime_r with UTC Z format — switched to gmtime_r
  • target_tokens * 4 overflow on untrusted input — capped
  • write_json_file() never checked write success — added out.good() check

Hardening

  • Generation counter prevents invalidate_recipes() from racing with in-progress Phase 2
  • computation_ok flag prevents caching failed recipe computation
  • Return value of get_system_info_with_cache() now copied under lock
  • KFD std::stoi failure now falls through to Priority 2 instead of aborting

Test results

  • lemond + lemonade build: zero warnings, zero errors
  • Server startup: no hang, cache built in ~750ms
  • CUDA Arch Mapping: 10/10 pass
  • Device Family Matching: 5/5 pass
  • System Info endpoint returns correct recipes

Pull

git pull https://github.com/bong-water-water-bong/1bit-lemonade.git pr-2443

🤖 Generated with Claude Code

@bong-water-water-bong

Copy link
Copy Markdown
Contributor

@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:pr-2443 into your startup_deadlock branch with the full diff pre-populated.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

I've opened PR #2449 with this fix: #2449

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>
@ckuethe

ckuethe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

pulled your bong-water-water-bong@a7fb81e into this PR

Copilot AI added a commit to ckuethe/lemonade that referenced this pull request Jun 27, 2026
- 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
@ckuethe

ckuethe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback @bong-water-water-bong

@jeremyfowers

Copy link
Copy Markdown
Member

Let's evaluate #2488 as a potentially simpler alternative.

@ckuethe

ckuethe commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

#2488 works too, and is a smaller, simpler diff than this one.

@ckuethe ckuethe closed this Jun 29, 2026
@ckuethe ckuethe deleted the startup_deadlock branch June 29, 2026 19:52
pull Bot pushed a commit to bhardwajRahul/lemonade that referenced this pull request Jun 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deadlock in get_system_info_with_cache prevents proper lemond startup

4 participants