Skip to content

Local chat model's Metal/GPU resources are never released on quit, reproducing the SIGABRT-on-quit crash class for chat sessions #2952

Closed

Description

@mstomar125

Problem

Any user who loads a local/agent chat model — the default config (ChatConfig::default().n_gpu_layers = GPU_OFFLOAD_ALL_LAYERS) fully offloads it to Metal — and then quits the app through the normal Quit path (NSApplication terminate: → exit(), or the tray's ControlFlow::Exit → process::exit(0)) will hit the same class of SIGABRT crash that previously took four dedicated fix commits (#864, #897, #885, #908) to eliminate for the embedding path. This is a real, previously-fixed-elsewhere crash class regressing on the chat/agent feature that was layered on afterward (#1025/#1042).

Root cause

register_atexit_handler() installs one process-wide libc::atexit callback shared by both services, per its own doc comment ("Shared between embedding and chat services since both load models into the same global llama.cpp backend"). But the cleanup() callback (packages/nlp-engine/src/embedding.rs:77-96) only iterates the embedding-only LLAMA_STATES registry before dropping the shared LLAMA_BACKEND. register_state_for_cleanup (line 104) is private and typed only for Arc<Mutex<Option<LlamaState>>> — it is structurally incompatible with ChatLlamaState, chat's distinct state type. ChatEngine::load_model (chat/mod.rs:290) only calls register_atexit_handler() to ensure the shared callback exists; it never registers self.state anywhere. Neither the daemon's graceful-shutdown GPU drain (drain_and_release_gpu, embedding-only) nor LocalAgentService::shutdown() (which only cancels tokens, never touches the underlying ChatEngine) release a loaded chat model either. So exit() skips ordinary Rust Drop, the atexit callback runs but never frees the chat model, and LLAMA_BACKEND gets torn down while ChatLlamaState's Metal residency sets are still live.

Suggested fix

Register ChatLlamaState in the same (or an analogous) cleanup registry used for the embedding path — either generalize LLAMA_STATES to hold both state types via a trait object/enum, or add a second CHAT_LLAMA_STATES registry plus a cleanup loop that runs inside the same atexit callback before LLAMA_BACKEND is dropped, mirroring register_state_for_cleanup's call in embedding's load_model path.


Found via the 2026-09-25 full-repo code sweep (finding #2 of 84 in CODE_SWEEP_FINDINGS_2026-09-25.md). Severity: critical. Category: broken.

Activity

  1. malibio commented on Sep 25, 2026

    @malibio
    Collaborator

    Frontend: 265 files / 5293 tests passed

  2. self-assigned this
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

backendsweepFound via the 2026-09-25 full-repo code sweep

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions