fix(tree-of-thought): avoid Context::fork() to fix output corruption on Vulkan/Windows#462
Conversation
…on Vulkan/Windows Context::fork() causes the model to produce incoherent/garbage output immediately after forking, even with a single fork (num_branches=1) and no concurrency. Plain chat completions without fork work correctly with the same model. Workaround: each branch now creates a fresh Context::new() and replays prior turns as text (system + user/assistant history) instead of relying on KV-cache sharing via fork(). Verified coherent output with num_branches=1 and num_branches=2 (8 concurrent leaves) at 128 and 512 tokens/step on Qwen3-0.6B (Vulkan/RTX 2050). Tradeoff: loses KV-cache sharing fork() was meant to provide. The underlying fork() bug (likely in runtime/src/context/snapshot.rs GPU page copy) remains unfixed and may warrant a separate investigation.
WalkthroughThe tree-of-thought inferlet's parallel generation logic was refactored to replace ChangesTree-of-Thought Context Refactor
Sequence Diagram(s)sequenceDiagram
participant Level1 as Level1Task
participant Level2 as Level2Task
participant Level3 as Level3Task
participant Model
Level1->>Model: load model, propose_ctx.generate(propose prompt)
Model-->>Level1: propose text
Level1->>Level2: pass propose prompt + propose text
Level2->>Model: execute_ctx replay(user: propose, assistant: propose text, user: EXECUTE_PROMPT)
Model-->>Level2: execute text
Level2->>Level3: pass propose/execute prompts + texts
Level3->>Model: reflect_ctx replay(user: propose, assistant: propose text, user: execute, assistant: execute text, user: REFLECT_PROMPT)
Model-->>Level3: reflect text
Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
inferlets/tree-of-thought/src/lib.rs (1)
79-84: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winBatch replayed turns into one prefill.
system/user/assistant/cueonly buffer tokens, andgenerate()drains that buffer on the first step. Flushing after every replayed message adds several avoidable forward passes per node; build the full replay, callcue(), then generate.♻️ Proposed refactor
propose_ctx.system(sys_prompt); -propose_ctx.flush().await?; let propose_prompt = format!("{}{}", PROPOSE_PROMPT_TEMPLATE, question_); propose_ctx.user(&propose_prompt); -propose_ctx.flush().await?; propose_ctx.cue(); // ... execute_ctx.system(sys_prompt); -execute_ctx.flush().await?; execute_ctx.user(&propose_prompt_); -execute_ctx.flush().await?; execute_ctx.assistant(&propose_text_); -execute_ctx.flush().await?; execute_ctx.user(EXECUTE_PROMPT); -execute_ctx.flush().await?; execute_ctx.cue(); // ... reflect_ctx.system(sys_prompt); -reflect_ctx.flush().await?; reflect_ctx.user(&propose_prompt_3); -reflect_ctx.flush().await?; reflect_ctx.assistant(&propose_text_3); -reflect_ctx.flush().await?; reflect_ctx.user(EXECUTE_PROMPT); -reflect_ctx.flush().await?; reflect_ctx.assistant(&execute_text_3); -reflect_ctx.flush().await?; reflect_ctx.user(REFLECT_PROMPT); -reflect_ctx.flush().await?; reflect_ctx.cue();Also applies to: 102-110, 129-141
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@inferlets/tree-of-thought/src/lib.rs` around lines 79 - 84, The replay in `propose_ctx` is doing separate flushes after `system` and `user`, which causes extra forward passes instead of one prefill. Update the replay flow in the `propose_ctx` setup to buffer all replayed turns first, then call `cue()` once before `generate()`, and apply the same batching approach to the other replay blocks referenced by `propose_ctx` so the context is built in one pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@inferlets/tree-of-thought/src/lib.rs`:
- Around line 79-84: The replay in `propose_ctx` is doing separate flushes after
`system` and `user`, which causes extra forward passes instead of one prefill.
Update the replay flow in the `propose_ctx` setup to buffer all replayed turns
first, then call `cue()` once before `generate()`, and apply the same batching
approach to the other replay blocks referenced by `propose_ctx` so the context
is built in one pass.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b9e1b117-3918-4870-a71e-3ea0bc2c6d2c
📒 Files selected for processing (1)
inferlets/tree-of-thought/src/lib.rs
Summary
Context::fork()causes the model to produce incoherent/garbage output immediately after forking, even with a single fork (num_branches=1) and no concurrency. Plain chat completions without fork work correctly with the same model.Root cause
Traced to
CopyD2D(viaggml_backend_tensor_get/setwith non-zero offsets) silently returning garbage on the Vulkan backend. Filed as issue #418.Fix
Each branch now creates a fresh
Context::new()and replays prior turns as text (system + user/assistant history) instead of relying on KV-cache sharing viafork().Verified coherent output with
num_branches=1andnum_branches=2(8 concurrent leaves) at 128 and 512 tokens/step on Qwen3-0.6B (Vulkan/RTX 2050).Tradeoff
Loses the KV-cache sharing
fork()was meant to provide. The underlyingfork()bug (likely inruntime/src/context/snapshot.rs, GPU page copy) remains unfixed and may warrant a separate investigation.Summary by CodeRabbit