feat: add ViT encoder ACL Graph capture for Qwen3-VL.#1866
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces VisionEncoderAclGraphManager to support ACL graph execution for the vision encoder in Qwen3-VL, along with corresponding configuration options and integration into the VLM executor and model. The review feedback identifies several critical issues, including a redundant dynamic_cast, potential race conditions from incorrect stream synchronization during graph capture, and graph replay bugs caused by changing tensor memory addresses with set_data. Additionally, the reviewer recommends refactoring the hardcoded deepstack outputs to a dynamic vector and addresses multiple style guide violations regarding braces, final classes, relative includes, auto usage, constant argument annotations, and fixed-width integers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| auto* vlm_model = dynamic_cast<CausalVLM*>(model_); | ||
| if (vlm_model) { | ||
| vlm_model->init_encoder_graph_manager(args, device); |
There was a problem hiding this comment.
| bool need_restore = false; | ||
| if (c10_npu::getCurrentNPUStream(device_index_) == | ||
| c10_npu::getDefaultNPUStream(device_index_)) { | ||
| c10_npu::setCurrentNPUStream(*capture_stream_); | ||
| aclrtSynchronizeStream(capture_stream_->stream()); | ||
| need_restore = true; | ||
| } |
There was a problem hiding this comment.
If need_restore is false (meaning the current stream was already a non-default stream), the capture stream is not switched to capture_stream_. However, at the end of capture, aclrtSynchronizeStream(capture_stream_->stream()) is still called. This synchronizes the wrong stream, which can lead to race conditions or undefined behavior. We should record the stream actually used for capture and synchronize it.
| bool need_restore = false; | |
| if (c10_npu::getCurrentNPUStream(device_index_) == | |
| c10_npu::getDefaultNPUStream(device_index_)) { | |
| c10_npu::setCurrentNPUStream(*capture_stream_); | |
| aclrtSynchronizeStream(capture_stream_->stream()); | |
| need_restore = true; | |
| } | |
| bool need_restore = false; | |
| if (c10_npu::getCurrentNPUStream(device_index_) == | |
| c10_npu::getDefaultNPUStream(device_index_)) { | |
| c10_npu::setCurrentNPUStream(*capture_stream_); | |
| aclrtSynchronizeStream(capture_stream_->stream()); | |
| need_restore = true; | |
| } | |
| aclrtStream capture_raw_stream = c10_npu::getCurrentNPUStream(device_index_).stream(); |
| } | ||
|
|
||
| // Verify with test replay | ||
| aclrtSynchronizeStream(capture_stream_->stream()); |
There was a problem hiding this comment.
| auto cu_tensor = torch::tensor( | ||
| param.cu_seqlens_vec, | ||
| torch::TensorOptions().device(device_).dtype(torch::kInt32)); | ||
| param.cu_seqlens.set_data(cu_tensor); |
There was a problem hiding this comment.
Using param.cu_seqlens.set_data(cu_tensor) changes the underlying memory address of the tensor. Since the ACL/NPU graph captures the physical memory addresses of tensors at capture time, changing the address during replay means the graph will continue to read from the old address (which may contain stale data or have been deallocated), leading to incorrect results or crashes. Instead, you should copy the data into the existing tensor to preserve its memory address when the shapes match.
auto cu_tensor = torch::tensor(
param.cu_seqlens_vec,
torch::TensorOptions().device(device_).dtype(torch::kInt32));
if (param.cu_seqlens.sizes() == cu_tensor.sizes()) {
param.cu_seqlens.copy_(cu_tensor);
} else {
param.cu_seqlens.set_data(cu_tensor);
}| torch::Tensor deepstack_out_0; // [budget/merge_sq, d_model] | ||
| torch::Tensor deepstack_out_1; // [budget/merge_sq, d_model] | ||
| torch::Tensor deepstack_out_2; // [budget/merge_sq, d_model] |
There was a problem hiding this comment.
Hardcoding deepstack_out_0, deepstack_out_1, and deepstack_out_2 makes the class fragile and limited to exactly 3 deepstacks. If a model has a different number of deepstacks, this will waste memory or fail. Refactor this to use a std::vector<torch::Tensor> to support any number of deepstacks dynamically.
| torch::Tensor deepstack_out_0; // [budget/merge_sq, d_model] | |
| torch::Tensor deepstack_out_1; // [budget/merge_sq, d_model] | |
| torch::Tensor deepstack_out_2; // [budget/merge_sq, d_model] | |
| std::vector<torch::Tensor> deepstack_outs; |
| device_index_(device.index()), | ||
| dtype_(args.dtype() == "bfloat16" ? torch::kBFloat16 : torch::kFloat16) { | ||
| max_budget_ = budgets_.empty() ? 0 : budgets_.back(); | ||
| capture_stream_.emplace(c10_npu::getStreamFromPool(true, device_index_)); |
There was a problem hiding this comment.
Annotate constant arguments with a comment indicating the parameter name when calling functions or constructors.
| capture_stream_.emplace(c10_npu::getStreamFromPool(true, device_index_)); | |
| capture_stream_.emplace(c10_npu::getStreamFromPool(/*isHighPriority=*/true, device_index_)); |
References
- Annotate constant arguments with a comment indicating the parameter name when calling functions or constructors. (link)
| { | ||
| LOG(INFO) << "[EncoderGraph] Warm-up run with budget=" << budget; | ||
| torch::Tensor warm_hidden = param.hidden_states.clone(); | ||
| for (int i = 0; i < static_cast<int>(layers.size()); ++i) { |
There was a problem hiding this comment.
Use fixed-width integers (int32_t, int64_t) instead of plain int.
| for (int i = 0; i < static_cast<int>(layers.size()); ++i) { | |
| for (int32_t i = 0; i < static_cast<int32_t>(layers.size()); ++i) { |
References
- Use fixed-width integers (
int32_t,int64_t) instead of plainint. (link)
|
|
||
| // Run 27-layer encoder loop + deepstack mergers | ||
| torch::Tensor hidden = param.hidden_states; | ||
| for (int i = 0; i < static_cast<int>(layers.size()); ++i) { |
There was a problem hiding this comment.
Use fixed-width integers (int32_t, int64_t) instead of plain int.
| for (int i = 0; i < static_cast<int>(layers.size()); ++i) { | |
| for (int32_t i = 0; i < static_cast<int32_t>(layers.size()); ++i) { |
References
- Use fixed-width integers (
int32_t,int64_t) instead of plainint. (link)
| ? "true" | ||
| : "false") | ||
| : "N/A"); | ||
| for (int idx = 0; idx < static_cast<int>(layers_.size()); ++idx) { |
There was a problem hiding this comment.
Use fixed-width integers (int32_t, int64_t) instead of plain int.
| for (int idx = 0; idx < static_cast<int>(layers_.size()); ++idx) { | |
| for (int32_t idx = 0; idx < static_cast<int32_t>(layers_.size()); ++idx) { |
References
- Use fixed-width integers (
int32_t,int64_t) instead of plainint. (link)
| deepstack_visual_indexes_.end(), | ||
| idx); | ||
| if (it != deepstack_visual_indexes_.end()) { | ||
| int index = std::distance(deepstack_visual_indexes_.begin(), it); |
There was a problem hiding this comment.
Use fixed-width integers (int32_t, int64_t) instead of plain int, and use static_cast for type conversions.
| int index = std::distance(deepstack_visual_indexes_.begin(), it); | |
| int32_t index = static_cast<int32_t>(std::distance(deepstack_visual_indexes_.begin(), it)); |
References
- Use fixed-width integers (
int32_t,int64_t) instead of plainint. (link)
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping. Co-Authored-By: Claude <noreply@anthropic.com>
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping.
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Fix non-NPU backend build: the has_init_encoder_graph_manager SFINAE trait was defined inside #if defined(USE_NPU) but is referenced unconditionally from CausalVLMImpl::init_encoder_graph_manager, so any non-NPU TU compiling causal_vlm.h failed with 'not a member of xllm::detail'. Moved the trait out of the USE_NPU guard (it uses only ModelArgs/torch::Device; on non-NPU no model provides the method, so it evaluates false and the if-constexpr body is discarded — encoder graph stays NPU-only at runtime). - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping.
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Fix non-NPU backend build: the has_init_encoder_graph_manager SFINAE trait was defined inside #if defined(USE_NPU) but is referenced unconditionally from CausalVLMImpl::init_encoder_graph_manager, so any non-NPU TU compiling causal_vlm.h failed with 'not a member of xllm::detail'. Moved the trait out of the USE_NPU guard (it uses only ModelArgs/torch::Device; on non-NPU no model provides the method, so it evaluates false and the if-constexpr body is discarded — encoder graph stays NPU-only at runtime). - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping. Post-build-verification: - Drop the qwen3.h direct #include of rotary_embedding_util.h that this commit had added: a clean build without it succeeds, so layer::rotary::get_concat_rotary_embedding is still resolved transitively (via npu_qwen3_decoder_layer_impl.h / llm_model_base.h). The include was a build side-effect of the CMakeLists change, not required by the feature. Co-Authored-By: Claude <noreply@anthropic.com>
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Fix non-NPU backend build: the has_init_encoder_graph_manager SFINAE trait was defined inside #if defined(USE_NPU) but is referenced unconditionally from CausalVLMImpl::init_encoder_graph_manager, so any non-NPU TU compiling causal_vlm.h failed with 'not a member of xllm::detail'. Moved the trait out of the USE_NPU guard (it uses only ModelArgs/torch::Device; on non-NPU no model provides the method, so it evaluates false and the if-constexpr body is discarded — encoder graph stays NPU-only at runtime). - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping. Post-build-verification: - Drop the qwen3.h direct #include of rotary_embedding_util.h that this commit had added: a clean build without it succeeds, so layer::rotary::get_concat_rotary_embedding is still resolved transitively (via npu_qwen3_decoder_layer_impl.h / llm_model_base.h). The include was a build side-effect of the CMakeLists change, not required by the feature. Co-Authored-By: Claude <noreply@anthropic.com>
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Fix non-NPU backend build: the has_init_encoder_graph_manager SFINAE trait was defined inside #if defined(USE_NPU) but is referenced unconditionally from CausalVLMImpl::init_encoder_graph_manager, so any non-NPU TU compiling causal_vlm.h failed with 'not a member of xllm::detail'. Moved the trait out of the USE_NPU guard (it uses only ModelArgs/torch::Device; on non-NPU no model provides the method, so it evaluates false and the if-constexpr body is discarded — encoder graph stays NPU-only at runtime). - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping. Post-build-verification: - Drop the qwen3.h direct #include of rotary_embedding_util.h that this commit had added: a clean build without it succeeds, so layer::rotary::get_concat_rotary_embedding is still resolved transitively (via npu_qwen3_decoder_layer_impl.h / llm_model_base.h). The include was a build side-effect of the CMakeLists change, not required by the feature. Production tuning (benefit-driven, folded in): 4-config (1/2/4/8 TP) x 7-token sweep showed graph only helps at small token counts on high-TP (collective-launch-bound, e.g. 8-card 256 +30.3%) and the captured encoder kernel is slower than eager past ~512 tokens (-3..-5%, TP-independent), so capture is capped at 512. - Hard cap: VisionEncoderAclGraphManager drops any budget > kEncoderGraphMaxReplayTokens (512) at construction via filter_budgets() with a WARNING; select_budget then returns -1 for actual_tokens > 512 so larger images fall back to eager. No config knob (cap is fixed). - Default --encoder_graph_budgets "1024,2048,4096,8192" -> "256,512" so the feature is useful out-of-box (small exact/near-exact buckets). - copy_inputs_to_persistent: drop the redundant full-budget zero_() on the head [0, actual) (immediately overwritten by copy_); zero only the padding tail [actual, bucket) when actual < bucket, since padding rows join the last segment's attention. Exact-budget does no zeroing. - Drop uncommitted ViT-forward timing diagnostics (Timer / [ViTForward] LOG / aclrtSynchronizeDevice) and the now-unused timer.h / acl_rt.h includes from qwen3_vl.h. - Trim verbose manager comments to concise one-liners. Verified: build_fia.sh incremental build succeeds; pre-commit clang-format passes. Co-Authored-By: Claude <noreply@anthropic.com>
Capture the vision encoder loop as an ACL Graph (NPUGraph) to cut operator dispatch latency and lower TTFT for VLM inference. The graph manager (core/runtime/vision_encoder_acl_graph_manager) is model-agnostic: it captures/replays purely through the VisionEncoderGraphAdapter interface in xllm::npu, so core/runtime never depends on a concrete model type. Qwen3_VisionTransformerImpl implements the adapter and registers itself via set_adapter(this). - Bucket-based lazy capture/replay with persistent input/output buffers - Stable-storage cu_seqlens: in-place copy_ into a pre-allocated device tensor (no set_data storage swap) so the captured graph reads a valid address on every replay - Segment-count guard: a request whose image count differs from the captured graph falls back to eager instead of corrupting attention - capture() wrapped in try/catch with stream restore + eager fallback on failure - head_dim sourced from args.mm_head_dim() (not derived) to support GQA - Config: --enable_encoder_graph, --encoder_graph_budgets Address PR xLLM-AI#1866 review feedback: - Fix capture-stream sync race: when the current stream was already non-default, capture ran on it but we synced capture_stream_ (wrong/idle stream). Record the raw stream actually used and synchronize that. - Remove redundant dynamic_cast<CausalVLM*>(model_) in VlmExecutorImpl (model_ is already CausalVLM*); null-check is sufficient. - Fix non-NPU backend build: the has_init_encoder_graph_manager SFINAE trait was defined inside #if defined(USE_NPU) but is referenced unconditionally from CausalVLMImpl::init_encoder_graph_manager, so any non-NPU TU compiling causal_vlm.h failed with 'not a member of xllm::detail'. Moved the trait out of the USE_NPU guard (it uses only ModelArgs/torch::Device; on non-NPU no model provides the method, so it evaluates false and the if-constexpr body is discarded — encoder graph stays NPU-only at runtime). - Style (per .agents/skills/code-review/references/custom-code-style.md): project-root-relative #include, class final, braces on single-line if, int32_t over plain int with static_cast, no auto for std::string, annotate constant args, clang-format line wrapping. Post-build-verification: - Drop the qwen3.h direct #include of rotary_embedding_util.h that this commit had added: a clean build without it succeeds, so layer::rotary::get_concat_rotary_embedding is still resolved transitively (via npu_qwen3_decoder_layer_impl.h / llm_model_base.h). The include was a build side-effect of the CMakeLists change, not required by the feature. Production tuning (benefit-driven, folded in): 4-config (1/2/4/8 TP) x 7-token sweep showed graph only helps at small token counts on high-TP (collective-launch-bound, e.g. 8-card 256 +30.3%) and the captured encoder kernel is slower than eager past ~512 tokens (-3..-5%, TP-independent), so capture is capped at 512. - Hard cap: VisionEncoderAclGraphManager drops any budget > kEncoderGraphMaxReplayTokens (512) at construction via filter_budgets() with a WARNING; select_budget then returns -1 for actual_tokens > 512 so larger images fall back to eager. No config knob (cap is fixed). - Default --encoder_graph_budgets "1024,2048,4096,8192" -> "256,512" so the feature is useful out-of-box (small exact/near-exact buckets). - copy_inputs_to_persistent: drop the redundant full-budget zero_() on the head [0, actual) (immediately overwritten by copy_); zero only the padding tail [actual, bucket) when actual < bucket, since padding rows join the last segment's attention. Exact-budget does no zeroing. - Drop uncommitted ViT-forward timing diagnostics (Timer / [ViTForward] LOG / aclrtSynchronizeDevice) and the now-unused timer.h / acl_rt.h includes from qwen3_vl.h. - Strip non-essential comments; keep only 2 (the 512-cap rationale and the padding-tail zero invariant) plus mandatory license headers. Verified: build_fia.sh incremental build succeeds; pre-commit clang-format passes. Co-Authored-By: Claude <noreply@anthropic.com>
新增配置开关(2 个)
--enable_encoder_graphfalse--encoder_graph_budgets"256,512"512 约束(澄清:非开关,不可配)
capture token 上限硬编码为 512(
kEncoderGraphMaxReplayTokens),不是配置开关、不可调:filter_budgets()丢弃任何 >512 的 budget(打LOG(WARNING);若全部超限则再告警 "graph effectively disabled")。select_budget对 actual_tokens > 512 自然返回 −1 → 回退 eager。--encoder_graph_budgets怎么配,>512 的图永远不会被捕获/服务。性能测试数据
Δ% = eager−graph,正=graph 快