Skip to content

Thread PrefillStrategy through generateUntilStop and the agent loop#238

Merged
michalharakal merged 2 commits into
developfrom
feature/prefill-strategy-agent-loop
Jul 18, 2026
Merged

Thread PrefillStrategy through generateUntilStop and the agent loop#238
michalharakal merged 2 commits into
developfrom
feature/prefill-strategy-agent-loop

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Closes #226.

What

Opt-in batched prefill for the agent/session path. Default behavior is unchanged (PrefillStrategy.Autoregressive).

  • generateUntilStop(...) gains prefillStrategy: PrefillStrategy = Autoregressive; Batched(maxBatch) ingests the prompt in chunks via forwardBatched, reporting onPrefill per chunk instead of per token
  • AgentConfig gains prefillStrategy, threaded through both AgentLoop call sites (JavaAgentLoop inherits it via AgentConfig — no facade change)
  • GenerationConfig gains a Java-friendly batchedPrefill(maxBatch) builder knob, threaded through both KLlamaSession.generate overloads
  • llm-agent and llm-runtime/kllama now expose llm-core as apiPrefillStrategy appears in their public signatures, and published-artifact consumers could not resolve it under implementation scope (first hit by Daily-StandAPP)

Why

generateUntilStop kept a hardcoded autoregressive prefill (with a comment pointing at the old position_collapse_bug.md divergence) long after the underlying issues were fixed — the head-reshape divergence by swapSeqHeadDims, batch-aware RoPE, and the cache-aware SDPA causal mask (maxKi = seqKV - seqQ + qi). llm-core's generate() already has PrefillStrategy; the agent path never got it, pinning every kllama consumer to one forward pass per prompt token.

Motivating consumer: Daily-StandAPP (JavaLand 2026 demo) measures 225–450 s per generation on CPU, dominated by prefill; the agent loop re-processes the full conversation every round, multiplying the cost.

Testing

New GenerateUntilStopPrefillEquivalenceTest pins greedy-sequence equivalence of batched vs autoregressive prefill through the exact entry point the agent loop uses — single-chunk and forced multi-chunk (maxBatch=3). All four prefill equivalence methods pass on real TinyLlama 1.1B Q8_0 weights (this test plus the existing PrefillStrategyEquivalenceTest), alongside the logit-level BatchedPrefillEquivalenceTest (max_abs_diff = 5.7e-6).

Note for CI: these model-loading tests want a quiet machine — two concurrent FP32 model loads alongside another large JVM reproducibly killed the test JVM before any assertion ran. Also includes PrefillTimingDiagnostic, a manual (non-asserting) wall-clock probe of autoregressive vs batched prefill at several chunk sizes.

Follow-ups (out of scope)

  • Consider flipping the default to Batched after a release of soak time
  • JavaAgentLoop.Builder.listener(AgentListener) — structured events (onToolCalls, onPrefillProgress, …) are wired internally but not exposed to Java consumers
  • Decode-side dominance: end-to-end agent-loop wall time also needs KV-cache reuse across rounds (each round currently does runtime.reset() + full re-prefill)

🤖 Generated with Claude Code

… the agent loop

generateUntilStop kept a hardcoded autoregressive prefill long after the
batched path reached parity (the head-reshape divergence was fixed by
swapSeqHeadDims, RoPE is batch-aware, and the CPU SDPA causal mask is
cache-aware: maxKi = seqKV - seqQ + qi). llm-core's generate() already
exposes PrefillStrategy; the agent/session path never got it - so every
kllama consumer (AgentLoop, JavaAgentLoop, KLlamaSession) was pinned to
one-forward-per-prompt-token prefill.

- generateUntilStop gains prefillStrategy (default Autoregressive,
  unchanged behavior); Batched ingests the prompt in maxBatch chunks via
  forwardBatched, reporting onPrefill per chunk
- AgentConfig gains prefillStrategy, threaded through both AgentLoop
  call sites (JavaAgentLoop inherits via AgentConfig)
- GenerationConfig gains prefillStrategy with a Java-friendly
  batchedPrefill(maxBatch) builder knob, threaded through both
  KLlamaSession.generate overloads
- GenerateUntilStopPrefillEquivalenceTest pins greedy equivalence of
  batched (single-chunk and forced multi-chunk) vs autoregressive
  prefill through the exact entry point the agent loop uses

Motivation: agent-loop consumers on CPU runtimes spend most of their
wall time in prefill - Daily-StandAPP (JavaLand 2026 demo,
https://github.com/michalharakal/Daily-StandAPP) measures 87s to first
token for a ~700-token tool-calling template on an M-series CPU;
batched prefill typically cuts prompt ingestion 3-10x.

Refs #226. Equivalence verified: BatchedPrefillEquivalenceTest,
PrefillStrategyEquivalenceTest, and the new
GenerateUntilStopPrefillEquivalenceTest all pass on TinyLlama 1.1B Q8_0
(greedy sequences identical, single-chunk and forced multi-chunk).
…ublic signatures

AgentConfig.prefillStrategy and GenerationConfig.prefillStrategy expose
llm-core's PrefillStrategy type in public API, but llm-agent and
llm-runtime/kllama declared llm-core as implementation — consumers of
the published artifacts could not resolve the type (first hit by
Daily-StandAPP wiring up batched prefill: 'Unresolved reference llm' /
'No parameter with name prefillStrategy').

Refs #226.
@michalharakal
michalharakal merged commit 9ab03e2 into develop Jul 18, 2026
2 checks passed
@michalharakal
michalharakal deleted the feature/prefill-strategy-agent-loop branch July 18, 2026 09:48
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.

Batched prefill never re-enabled in generateUntilStop / agent loop after position-collapse fixes

1 participant