Skip to content

fix(llama): release a vision chat handler's mtmd context on close (#2342)#2343

Open
Anai-Guo wants to merge 1 commit into
abetlen:mainfrom
Anai-Guo:fix/close-chat-handler-mtmd
Open

fix(llama): release a vision chat handler's mtmd context on close (#2342)#2343
Anai-Guo wants to merge 1 commit into
abetlen:mainfrom
Anai-Guo:fix/close-chat-handler-mtmd

Conversation

@Anai-Guo

Copy link
Copy Markdown
Contributor

Summary

Fixes #2342.

Llama.close() unwinds self._stack, which frees the model, context and batch — but the chat handler's own _exit_stack is never touched. That stack holds the mtmd_free callback for the mtmd/clip context, so a vision handler's context is simply leaked on close.

The crash in #2342 follows from that leak. A handler routinely outlives the Llama that initialized it (callers build one handler and reuse it across loads, as ComfyUI's keep_model_alive: false path does), and _init_mtmd_context() returns early while mtmd_ctx is set:

def _init_mtmd_context(self, llama_model: llama.Llama):
    if self.mtmd_ctx is not None:
        return  # Already initialized

The context was built from the first model (mtmd_init_from_file(..., llama_model.model, ...)). After close() frees that model, the handler still believes it is initialized and hands the stale context to the second Llama — which is where mtmd_ctx shows up null on the C++ side.

Fix

Register the handler's exit stack on the Llama's _stack in __init__:

handler_stack = getattr(chat_handler, "_exit_stack", None)
if handler_stack is not None:
    self._stack.callback(handler_stack.close)
  • Ordering is correct by construction. ExitStack unwinds LIFO and the model/context/batch are entered earlier in __init__, so the mtmd context is released before the model it was built from.
  • Re-initialization now works. mtmd_free sets self.mtmd_ctx = None, so the next load re-initializes against the new model instead of reusing a dangling one. ExitStack is reusable after close(), so the handler stays usable.
  • Covers both handler families. Duck-typed on _exit_stack, which Llava15ChatHandler and MTMDChatHandler — and therefore every subclass (Gemma4ChatHandler, Qwen25VLChatHandler, MiniCPMv26ChatHandler, …) — expose. getattr keeps plain non-vision handlers and chat_handler=None unaffected.

This is the upstream equivalent of the workaround the reporter applied downstream in _drop_local_llama.

Verification

I don't have the reporter's vision model to hand, so I verified the lifecycle contract directly by reproducing the object graph (handler with _exit_stack + mtmd_ctx early-return, Llama with an _exit_stack-based teardown) and driving two load/close cycles against it.

Without the fix, the second cycle skips re-initialization and reuses the context bound to the freed model — the reported failure. With it, the trace is:

init ctx@m1 / free ctx@m1 / free model m1
init ctx@m2 / free ctx@m2 / free model m2

mtmd freed ahead of the model on both cycles, and a fresh context on the second load.

Scope

One file, +10/-0, no behavior change for non-vision handlers.

🤖 Generated with Claude Code

…etlen#2342)

`Llama.close()` tore down the model and its context but left the chat
handler's `_exit_stack` untouched, so the mtmd/clip context it built from
that model was never freed.

Handlers routinely outlive the `Llama` that initialized them -- callers
construct one handler and reuse it across loads -- and
`_init_mtmd_context()` returns early while `mtmd_ctx` is set. After the
first `close()` the handler therefore kept a context bound to an
already-freed model and handed it back on the next load, where it
surfaces as a null `mtmd_ctx` on the C++ side.

Register the handler's exit stack on the Llama's `_stack`. It unwinds
LIFO, so the mtmd context is released ahead of the model teardown
registered earlier in `__init__`, and `mtmd_free` resets `mtmd_ctx` to
`None` so the next load re-initializes cleanly. Duck-typed on
`_exit_stack`, which both `Llava15ChatHandler` and `MTMDChatHandler`
(and every subclass) expose.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Llama.close() doesn't clean up chat_handler causing null pointer on second load of a vision model

1 participant