Skip to content

qwen27b-speculative-decoding-fix: fix long prompt crash and archive evidence files - #287

Open
Hinanaw-iTenshi wants to merge 33 commits into
mainfrom
main-fix
Open

Hinanaw-iTenshi wants to merge 33 commits into
mainfrom
main-fix

Conversation

@Hinanaw-iTenshi

@Hinanaw-iTenshi Hinanaw-iTenshi commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What

This PR provides a root-cause fix for long-prompt crashes in Qwen3.8-27B + DFlash2 speculative decoding, replacing the previous temporary 2048-token guard. The crash occurred because prompt hidden states were previously passed to common_speculative_process() once after full prefill completion, whereas the target hidden feature buffer is sized to n_batch and overwritten on each chunk decode. By processing common_speculative_process() incrementally per chunk after each chunk decode, prompts of arbitrary length are safely supported without memory corruption. The PR also updates reference architecture documentation with verified GPU offload measurements and relocates referenced verification artifacts from tmp/ into an untracked assert/ directory with .gitignore coverage.

Changes

  • Fix speculative decoding on long prompts (Step 2): Replaced the one-shot post-prefill common_speculative_process() call with per-chunk incremental processing in src/GenieAPIService/src/context/llama_cpp.cpp, removing the temporary kSpeculativeMaxPromptTokens=2048 guard. Verified on 32GB ARM64 hardware across GPU and CPU modes up to 8.4k prompt tokens without crashes or regressions.
  • Benchmarking and tuning parameters (Step 3): Extended test/benchmark_gguf_tuning.py with spec_draft_n_max, spec_draft_p_min, and repeated sampling for speculative decoding evaluations, ensuring config backups are cleanly discarded.
  • Archive reference evidence and gitignore (Step 4): Added assert/ directory to .gitignore to safely house 21 reference benchmark logs, dump analyses, and test scripts previously kept in tmp/, updating 20 document links across private developer notes.
  • Documentation and GPU offload metrics: Updated src/GenieAPIService/src/context/llama_cpp.md with verified hardware offload numbers (main model 53/66 layers + draft model 6/6 layers on GPU, ~97% VRAM utilization) and corrected memory accounting descriptions.

Hinanaw-iTenshi and others added 18 commits October 3, 2026 12:02
…physical memory gate for draft_model

llama_cpp.cpp: speculative n_ctx now prefers model_config_.get_context_size(), falls back to 32768 only when unset/0 (non-speculative path unchanged, still falls back to 8192). llama_speculative_config.h/.cpp: replace fixed 60GiB total-physical-memory threshold with EstimateSpeculativeMemoryRequirement() (weight file sizes + KV cache estimate using GGUF block_count as n_layer + fixed safety margin, calibrated against real llama-completion.exe PeakPagedMemorySize64 measurement) and GetAvailablePhysicalMemoryBytes() (compares against available, not total, physical memory). model_manager.cpp: GGUFVerify::CreateIfVerifiedImpl() calls the new dynamic estimator instead of the fixed threshold. model_manager.h: corrected now-inaccurate comment on kInsufficientMemory.

Co-authored-by: Junie <junie@jetbrains.com>
…branch so fit can auto-size GPU offload within device memory

Co-authored-by: Junie <junie@jetbrains.com>
…lazy-output detection

Co-authored-by: Junie <junie@jetbrains.com>
…/keyword judging in benchmark_gguf_tuning.py

Co-authored-by: Junie <junie@jetbrains.com>
- 新增 run_inception_precision_compare_task():只在首轮发送用户原文 prompt,
  不无条件追加续问;探测到 busy 时按既有 busy-poll 机制等待;探测到疑似
  Blocking Condition(_looks_like_blocking_question 启发式)时才追加唯一一轮
  最简中性继续指令,否则如实停止——用于验证'一次性自动跑完,无需中途确认'
  这句话本身是否成立。
- 新增 _find_inception_artifacts():三要素判定(FP16/W8A8模型目录存在、
  mtime>=任务开始时间的新鲜推理证据、对比陈述关键词),避免复用
  inception_v3/NOTES.md 里本来就有的旧数字蒙混过关。
- 新增 --task inception_precision_compare CLI 入口,与现有
  model_conversion/model_build_probe/tetris 模式一致。
- 仓库范围核查确认 test_builder_agent_tasks.py 是唯一的真实接入脚本;
  git log --all --follow 对 test_builder_cli.py/test_qaimodelbuilder_remote.py
  零命中,证实历史上不存在被遗漏的真实接入逻辑。
- 详细决策与核查证据记录在 test_builder_agent_tasks.py.notes.md(gitignored)。

Co-authored-by: Junie <junie@jetbrains.com>
…ng inception task

Co-authored-by: Junie <junie@jetbrains.com>
…ard elimination

Co-authored-by: Junie <junie@jetbrains.com>
…ist them in transcript/results.json

Co-authored-by: Junie <junie@jetbrains.com>
…ro-score L0 fallback implementation

Co-authored-by: Junie <junie@jetbrains.com>
…r per-chunk sub-inference

RunSummarizationInference() (shared by LongTextSummarizer::MapChunk/ReduceSummaries
and TaskMemoBuilder) had no independent timeout on context_->Query(). Real-machine
repro (tmp/step4_run1) showed the Qwen3.8-27B driver model stuck in a non-converging
<think> self-correction loop while trying to hit an exact summary length target,
consuming the whole turn budget and leaving the main dialog to end with no output.
Add a 300s steady_clock wall-clock budget to the per-token callback; on timeout the
callback returns false (reusing the existing should_stop clean-exit path, same as
client-disconnect detection), a new timed_out flag is checked before the existing
success check (Query() returns true regardless of should_stop vs eog_hit, so success
alone cannot distinguish a timed-out generation), and the function returns an empty
string instead of the partial (likely mid-<think>) output. Empty result triggers the
caller's existing summarization-failed/empty fallback (keep original chunk text),
so no caller-side changes are needed.

Co-authored-by: Junie <junie@jetbrains.com>
…usage field

ResponseTools::responseDataJson() previously hardcoded usage.{prompt_tokens,
completion_tokens,total_tokens} to 0 for every response, regardless of actual
generation. This caused downstream QAIModelBuilder to treat local-model
responses as an "empty usage block" and fall back to an unreliable tokenizer
estimate (confirmed via two independent real-machine runs of
inception_precision_compare producing an identical used_tokens=95 - a
deterministic-bug fingerprint, not model randomness).
Fix: responseDataJson() gains two optional trailing params
(prompt_tokens/completion_tokens, default 0, preserving existing behavior for
mid-stream content chunks per OpenAI streaming semantics). In SendResponse(),
reuse handle->HandleProfile() (already implemented by all 3 backends -
GGUF/MNN/QNN - for logging) to extract real num_prompt_tokens/
num_generated_tokens once per request, wired into the 3 finish/final response
call sites (connection_broken end frame, normal end frame, non-streaming final
response). Safe fallback to 0 on missing/non-numeric fields.

Co-authored-by: Junie <junie@jetbrains.com>
…long agentic chains

Co-authored-by: Junie <junie@jetbrains.com>
…for local-only agentic tasks

Co-authored-by: Junie <junie@jetbrains.com>
…o avoid premature history eviction of key facts

Run9 real-machine test (Qwen3.8-27B+DFlash2, 16384 ctx) showed messages
capped at 16 while token usage was only 29%-65%; this evicted the
model's just-read qairt_env.json tool result (correct QAIRT SDK path),
leaving only a bare numeric placeholder with no Task Memo fallback.
The model then hallucinated a wrong tool path and fell into a
PowerShell search loop until per_turn_timeout. Raising the count limit
lets the 16384-token context budget become the binding constraint
instead of message count.

Co-authored-by: Junie <junie@jetbrains.com>
…-aware Step1 trimming

- service_config.json: add prompt_optimization.task_memo (enabled=true, model_layer_enabled=false) - rule layer only, zero extra inference, 1.5x memory-frontier gain per docs/compression-reliability-assessment.md; task_memo loading code in model_manager.cpp already existed, unused until now.
- message_pre_filter.cpp PreFilterMessages: Step 1 (max_messages_limit) now only trims when message count exceeds the limit AND token usage ratio reaches new token_pressure_trigger_ratio (default 0.75); previously trimmed purely by count regardless of token budget (Run9 repro: trimmed at 41% token usage, discarding a tool result the model needed, causing hallucination). Moved handle/system_tokens/available_tokens/calc_msg_tokens up before Step 1 to support this; removed the now-duplicate later calc_msg_tokens definition.

Co-authored-by: Junie <junie@jetbrains.com>
…h on long prompts via per-chunk incremental common_speculative_process (replaces token-limit guard)

Root cause: common_speculative_process() reads the target-side hidden-layer
capture buffer sized to n_batch; every llama_decode overwrites it from row 0.
The prefill loop already decodes the prompt in n_batch-sized chunks (correct),
but the previous implementation called common_speculative_process() only once,
after all chunks, with the entire prompt -- once the prompt exceeded one
n_batch, that call read stale/out-of-bounds data left over from the last
chunk's decode. Upstream common_speculative_begin()'s warning comment confirms
that calling process() after every prefill chunk is the only correct way to
drive this API; there is no "process the whole prompt once" alternative.

Fix: call common_speculative_process() immediately after each chunk's
llama_decode, using that chunk's own tokens with correctly offset pos values;
removed the old one-shot post-prefill call. This supersedes the hard
2048-token guard added earlier in this same commit, which only masked the
symptom by rejecting long requests outright instead of fixing the call
pattern.

Validated on remote ARM64 (full rebuild): GPU up to 8430 prompt tokens, CPU up
to 6152 tokens, all HTTP 200 with coherent output and no crash; short-prompt
decode speed unaffected (two repeats within noise of each other, no
regression). There is no longer any speculative-decoding token limit
independent of n_ctx.

Deployment note (cost a full extra debugging detour before being found):
after rebuilding, all dependent DLLs (llama-common.dll,
llama-completion-impl.dll, GenieAPILibrary.dll) must be redeployed alongside
the exe. Redeploying only GenieAPIService.exe silently leaves the process
running stale logic and produces a false "fix doesn't work" result.
…parameters and repeat sampling to benchmark tool

Support spec_draft_n_max, spec_draft_p_min, and repeated question sampling in benchmark_gguf_tuning.py to evaluate speculative decoding tuning configurations.
discard config backup file after restoration in benchmark runner
Call discard_backup after restore to ensure no bench_orig_backup files remain in model directory.

Co-authored-by: Junie <junie@jetbrains.com>
…ere before tmp/ cleanup)

Co-authored-by: Junie <junie@jetbrains.com>
@Hinanaw-iTenshi Hinanaw-iTenshi changed the title qwen3-27b-speculative-decoding: fix long prompt crash and benchmark draft parameters qwen27b-speculative-decoding-fix: fix long prompt crash and archive evidence files Oct 6, 2026
…iscarding context

Discarding earlier context (sliding-window eviction) to let speculative
generation continue past n_ctx was rejected: no previously-generated content
should be dropped, and the limit the service honors should be the model's
full configured n_ctx, not a value generation silently works around by
forgetting its own output. Reused the existing stopped_by_output_limit_ flag
(maps to finish_reason=length, the same mechanism already used when
max_length is hit) so the speculative generation loop stops cleanly before
decoding a batch that would exceed n_ctx, instead of evicting KV cache
entries from either the target or draft context. Checked upstream first:
4453b535f and other recent fork commits touching speculative decoding only
fix layer-input batch-order preservation, unrelated to context-limit
handling, so there was no existing fix to adopt instead.

Co-authored-by: Junie <junie@jetbrains.com>
@Hinanaw-iTenshi
Hinanaw-iTenshi force-pushed the main-fix branch 3 times, most recently from 15cad6e to 7cbab2b Compare October 7, 2026 04:11
…e prompt, and lightweight page directory to Task Memo

Co-authored-by: Junie <junie@jetbrains.com>
Hinanaw-iTenshi and others added 3 commits October 7, 2026 15:05
…ault

tool_call_repair is a purely local tool-call retry/circuit-breaker mechanism with no cloud relevance, so it no longer belongs in service_config.json (which should stay scoped to edge-cloud routing). Removed the JSON section and its parsing block in model_manager.cpp; ToolCallRepairConfig now relies solely on its struct member defaults in model_config.h. Updated architecture.md and stale comments referencing the removed config field.

Co-authored-by: Junie <junie@jetbrains.com>
service_config.json 收窄为端云结合最小配置 + 下游客户端私货文案(按客户端要求保留,
不迁出);通用本地算法参数(消息裁剪/紧急截断/长文本摘要/Task Memo/技能目录格式等)、
本地调试开关(status_update_content_visible/log_inference_stream),以及此前硬编码的
tool_call_repair(恢复为可配置项,因与端云无关故不回归 service_config.json),
全部迁至新建的独立文件 prompt_engineering.json,由单独加载流程无条件解析,不受 -n 门控。
log_rule_matches 调试开关归位到 routing.sensitivity_detection.debug_log_matches(它是
安全检测模块自身的子开关,此前错误地放在顶层 debug 节)。

同步更新 test_service.py 中依赖这些字段路径的测试基础设施(_read_long_text_trigger_ratio/
_ScArmConfigOverride/_LtmArmConfigOverride 及相关帮助文本),以及 CMakeLists.txt 的三处
构建产物拷贝命令,确保 prompt_engineering.json 随 service_config.json 一起部署。

已在远程 ARM64 机器上验证 GenieAPIService.exe 编译通过(exit_code=0)。
docs/architecture.md 对应章节已改写为准确反映当前归属(本机私有文档,不随此提交同步)。

Co-authored-by: Junie <junie@jetbrains.com>
…dating Qwen3.8-27B GGUF/GPU DFlash2 speculative decoding on QAIModelBuilder agent tasks

- model_input_builder.h/long_text_summarizer.cpp: Qwen3.5-generation models
  (confirmed via official model card) do not honor the /think //no_think soft
  switch; replace it with the official hard switch (prefilling an empty
  <think></think> block) gated on is_thinking_model() &&
  !getenableThinking() && is_speculative_draft_configured(), applied to both
  the main prompt path and the LongTextSummarizer Map/Reduce sub-inference
  prompts.
- llama_cpp.cpp: tighten the --reasoning-budget CLI arg to
  is_thinking_model() only; pair the forced reasoning-budget closing branch's
  llama_decode() call with common_speculative_process() to keep the
  drafter's hidden-state capture buffer position watermark in sync, per the
  invariant already required by the per-chunk prefill fix.
- chat_request_handler/tool_call_repetition_store.{h,cpp}(+selftest),
  response_dispatcher.cpp, task_memo_builder.{h,cpp}: new session-scoped
  LRU+TTL store that detects exact-signature repeated tool calls and injects
  a one-shot corrective note via Task Memo (or a standalone fallback) once a
  repeat threshold is hit.
- model_config.h/model_instance_config.h/model_manager.cpp/
  prompt_engineering.json: add speculative_draft_configured_ flag (derived
  from config.json's draft_model block) and a general, model-agnostic
  Windows shell usage hint (cmd.exe vs PowerShell syntax) injected when a
  request declares exec/shell-like tools on Windows builds.
- test/test_builder_agent_tasks.py: widen the SSE client read timeout
  (10,25)->(15,60) to clear the server's 15s heartbeat interval under heavy
  real workloads; make the four budget-related argparse defaults None so
  they stop silently overriding each task function's own tuned defaults; add
  a stream_timeout-triggered neutral continuation so a client-side SSE
  deadline no longer prematurely ends a still-healthy turn.

4 complete real-machine reruns of inception_precision_compare with
qwen3.8-27b-q4_0 (step3_rerun/step3_final_rerun/step3_r3_final/
step3_final_v2) confirm zero crashes, zero protocol/circuit-breaker
anomalies, and zero context-overflow truncations across all rounds. The
three-factor acceptance criteria were not met in any round; the final
observed bottleneck is the model spending an entire turn re-reading skill/
guardrail documentation before reaching the real conversion commands, which
is a model-behavior/test-policy question (not a code defect), recorded as
a new open issue.

Co-authored-by: Junie <junie@jetbrains.com>
Hinanaw-iTenshi and others added 7 commits October 9, 2026 11:12
真机取证推翻了"模型只读文档不执行"这一归因:模型始终在发执行类调用,是服务端在它
看不见的地方逐级削弱了它的行动能力。四处根因与对应修复:

- prompt_optimizer.cpp: BuildRelevanceKeywords() 把 messages 首条 user 消息作为任务
  锚点无条件纳入关键词语料(anchor_first_user_message),多轮工具循环里滚动窗口只剩
  assistant/tool 时关键词不再变空;AssignSkillDetailLevels() 补齐"无法判断相关性"这
  第三态,keywords 为空时不再把技能目录整体拍扁到最低档;FilterToolsByRelevance() 新
  增核心能力工具豁免(protect_core_capability_tools),文件读写/路径检索/命令执行类工
  具不再因相关性零分被整条删除(仍无条件受 token 预算约束)。
- message_pre_filter.cpp: Phase 4 紧急截断改为在受保护区间内按 token 降序多目标选取
  (target_largest_tool_messages),并把候选集从"仅 role=tool"扩展到带 tool_calls 的
  assistant 消息正文——后者走结构感知截断,只收缩 arguments 里体量最大的那个字符串值,
  name 与 arguments 的键结构逐字节不动,畸形信封直接跳过而非输出半截 JSON;同时修正
  Available 的 size_t 下溢与截断日志的引用别名缺陷。
- prompt_engineering.json: 新增 relevance_filter 节与 target_largest_tool_messages 开
  关(均默认开启、可逐字节回退);重写 windows_shell_hint.hint_text,改为给可直接照抄
  的正反范式(每条正例都已在真实 PowerShell 上验证过);摘要缓存 ttl_minutes 60->180
  以覆盖单轮长任务全程。
- service_config.json: max_tool_call_retries 40->80。按工具调用次数硬截断只是失控的粗
  粒度近似,真正的失控判据是已有的四套无进展信号;实测合法长链路任务需 55-70 次。
- test_service.py: 两处把 prompt_engineering.json 当成 <exe_dir> 根目录文件的路径 bug
  修正为 <exe_dir>/config/,此前导致相关套件静默跑在默认配置上。

真机连续三轮复测依次解除三道成因各不相同的中断(紧急截断只看最后一条消息 -> 候选集漏
掉 assistant 正文 -> 次数上限),三轮均 crashed=0、零续问、零阻断命中,最终真实驱动完
整模型转换流水线并落盘首个上下文二进制产物。

Co-authored-by: Junie <junie@jetbrains.com>
…00 拦截与 tool_call_limit 回退分支),防失控交给按进展判定的机制

Co-authored-by: Junie <junie@jetbrains.com>
- context/llama_cpp.cpp|.h, context/context_base.h: 跨请求保留上次已 decode 的 token
  序列,新请求按最长公共前缀对 target 与 DFlash draft 两侧 memory 调 seq_rm 裁剪,只
  prefill 新增尾部;模型切换、Stop() 中断、上下文超限、前缀长度为 0 或任何异常时回退到
  完全清空路径。开关为 prompt_engineering.json 的 llama_prefix_cache.enabled(默认开)。
  同时补齐投机解码 proposed/accepted 计数与接受率,并按 query 导出追踪指标。
- chat_request_handler/request_trace_writer.h 及 chat_request_handler.cpp、prompt_ledger.h、
  prompt_stats_helper.h: 每个请求写一行 JSONL(请求 id、模型、最终 prompt 全文、原始输出、
  解析出的 tool_calls、prompt/生成 token 数、prefix_reused_tokens、prefix_reset_reason、
  messages_dropped_prefix_len、TTFT、prefill/decode 速率、投机接受率、PromptLedger 摘要),
  由 prompt_engineering.json 的 debug.request_trace 开关与输出路径控制,默认关闭。
- chat_request_handler/message_pre_filter.cpp|.h: 驱逐状态按原始消息下标与原始内容核验
  (不再对压缩后内容做哈希,否则每轮必然失配);驱逐边界只能单调前进;Phase 4 紧急截断长度
  量化到固定档位且跨轮只能变小;calc_msg_tokens 与批量丢弃都计入 tool_calls 结构占用;存活
  非 system 消息条数设下限保护;重复工具调用结果以索引中的已有摘要做短路桩替换。
- chat_request_handler/task_memo_builder.cpp|.h, model_input_builder.h: Task Memo 记录已读
  文件路径与截断摘要并渲染为独立段落,对存活消息中的重复读取追加提示。
- chat_history/*, model/*, response/*: 配合上述改动的接口透传与配置读取。
- tool/analyze_request_trace.py: 离线分析追踪 JSONL,统计重复工具调用、重复读取、失败重试、
  每轮耗时与 token 占比,并按 prefix_reset_reason 分组给出 prefill 均值。

Co-authored-by: Junie <junie@jetbrains.com>
…ion_tokens=1、生成速率 0 的错误

Co-authored-by: Junie <junie@jetbrains.com>
….json to match other GGUF models

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
- message_pre_filter: 重复调用按首份存活与 40% 预算事前决策 stub/refill,决策跨轮固定;被重读结果驱逐时保留(常规 <=20%,含 must_keep 合计 <=40%),must_keep 首份不被 Drop/1A/1C/2A/2B/存活下限兜底压缩;SmartSelect 预判触发时全部 refill;新增 [RepeatRead] 日志
- message_pre_filter: prefix_hash 对称修复(msg_raw_for_hash 在占位改写前拷贝)
- task_memo_builder: Render 超预算块 continue,read_files 前移,redundant_call_guard 文案如实反映是否仍在上下文

Co-authored-by: Junie <junie@jetbrains.com>
@Hinanaw-iTenshi
Hinanaw-iTenshi force-pushed the main-fix branch 2 times, most recently from c81499e to 8c5796c Compare October 12, 2026 23:53
…servable

response_dispatcher: pass request_data into the Layer2 retry ModelInputBuilder so the retry path keeps skill catalog, relevance keywords, tool filtering and task memo. message_pre_filter: fold OpenAI tool_calls back into dropped-message content so TaskMemoBuilder can recognise read tool calls. llama_cpp: report prefix cache miss mode/n_cached/limit/common_prefix. task_memo_builder: report memo blocks skipped for budget. model_input_builder: report system prompt rebuild delta instead of mislabelled savings. test: FP16-only builder task variant, genie_service.log curve parser, per-turn timeout now follows the total deadline, existing_run run_id is recorded and the blocking-question heuristic no longer fires on a bare question mark.

Co-authored-by: Junie <junie@jetbrains.com>
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.

1 participant