-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[None][feat] Opt GPT-OSS in to KV cache manager V2 by default #16942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| from typing import Any, Dict, Literal, Optional | ||
| from typing import TYPE_CHECKING, Any, Dict, Literal, Optional | ||
|
|
||
| import torch | ||
| from torch import nn | ||
|
|
@@ -32,6 +32,9 @@ | |
| from .modeling_speculative import SpecDecOneEngineForCausalLM | ||
| from .modeling_utils import DecoderModel, filter_weights, register_auto_model | ||
|
|
||
| if TYPE_CHECKING: | ||
| from tensorrt_llm.llmapi.llm_args import TorchLlmArgs | ||
|
|
||
| # Use TinyGEMM when the number of tokens is not larger than this threshold | ||
| MIN_LATENCY_TINYGEMM_NUM_TOKENS = 128 | ||
|
|
||
|
|
@@ -552,6 +555,25 @@ def forward( | |
| @register_auto_model("GptOssForCausalLM") | ||
| class GptOssForCausalLM(SpecDecOneEngineForCausalLM[Transformer, GptOssConfig]): | ||
|
|
||
| @classmethod | ||
| def get_model_defaults(cls, llm_args: "TorchLlmArgs") -> dict: | ||
| """Select KV cache manager V2 by default. | ||
|
|
||
| GPT-OSS applies a sliding window to every other layer | ||
| (see ``AttentionBlock.__init__``), so the KV cache is VSWA: two | ||
| distinct attention window sizes. V2 groups layers by lifecycle and | ||
| coalesces buffers within each pool group, which sizes the | ||
| sliding-window and full-attention pools independently instead of | ||
| statically dividing memory between them. | ||
|
|
||
| Users keep full control: an explicit | ||
| ``kv_cache_config.use_kv_cache_manager_v2`` always wins over this | ||
| default. Two-model Eagle3 is a known exception -- V2 does not split | ||
| the KV cache budget between the target and draft managers -- so that | ||
| combination should be run with the flag set to False explicitly. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checking: this method is for setting good defaults (e.g. for perf), but AFAICT, it does note forbid incompatible / unsupported options. The wording in this docstring suggests there are situations under which this has to be set to a certain value -> should that be checked for in the constructor? |
||
| """ | ||
| return {"kv_cache_config": {"use_kv_cache_manager_v2": True}} | ||
|
|
||
| @classmethod | ||
| def get_preferred_transceiver_runtime( | ||
| cls, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -563,10 +563,16 @@ def _resolve_kv_cache_manager_v2_auto( | |
| original_setting: Optional[Union[bool, str]] = None) -> bool: | ||
| """Resolve the KV cache manager auto setting after model defaults are applied. | ||
|
|
||
| The transceiver runtime auto setting must be resolved first. In | ||
| disaggregated serving, hybrid Mamba V2 requires the Python transceiver with | ||
| NIXL, so an incompatible route falls back to V1 unless the user explicitly | ||
| selected V2. | ||
| A model default of V2 is demoted to V1 for routes V2 cannot serve; an | ||
| explicit user value always wins. The compatibility arms are: | ||
|
|
||
| - Disaggregated serving: hybrid Mamba V2 requires the Python transceiver | ||
| with NIXL, so any other route falls back to V1. The transceiver runtime | ||
| auto setting must be resolved first. | ||
| - Two-model speculative decoding: the draft model runs in a separate engine | ||
| with its own KV cache manager, and V2 cannot split the KV cache budget | ||
| between the two -- both managers would size their pools from the full | ||
| budget and double-allocate it -- so it falls back to V1. | ||
| """ | ||
| setting = (llm_args.kv_cache_config.use_kv_cache_manager_v2 | ||
| if original_setting is None else original_setting) | ||
|
|
@@ -595,6 +601,22 @@ def _resolve_kv_cache_manager_v2_auto( | |
| "falling back to V1.", runtime, effective_backend) | ||
| model_default = False | ||
|
|
||
| spec_config = llm_args.speculative_config | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This arm fires for every model with a V2 default, not just GPT-OSS — NemotronH, Qwen3-Next, and DeepSeek-V4 with two-model Eagle3 / draft-target / MTP-Eagle all switch from V2 to V1 as of this PR. That's plausibly the right call, but it's a behavior change for those models that isn't in the PR description and has no test outside the GPT-OSS file. Please state it in the description and consider a parametrized test over the V2-default model classes so a future model adding |
||
| if model_default and spec_config is not None: | ||
| # ``has_draft_model()`` is the same predicate py_executor_creator uses | ||
| # to decide whether to build a separate draft model engine, which is | ||
| # what forces the second KV cache manager. | ||
| spec_dec_mode = getattr(spec_config, "spec_dec_mode", None) | ||
| if spec_dec_mode is not None and spec_dec_mode.has_draft_model(): | ||
| decoding_type = getattr(spec_config, "decoding_type", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The justification ("both managers would size their pools from the full budget and double-allocate it") doesn't obviously match |
||
| "speculative decoding") | ||
| logger.info( | ||
| "KV cache manager V2 is the model default, but %s runs the " | ||
| "draft model in a separate engine and V2 cannot split the KV " | ||
| "cache budget between the target and draft managers; falling " | ||
| "back to V1.", decoding_type) | ||
| model_default = False | ||
|
|
||
| llm_args.kv_cache_config.use_kv_cache_manager_v2 = model_default | ||
| return model_default | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,11 @@ | |
| from tensorrt_llm._torch.pyexecutor.resource_manager import KVCacheManager | ||
| from tensorrt_llm.bindings.executor import \ | ||
| KvCacheConfig as BindingsKvCacheConfig | ||
| from tensorrt_llm.llmapi import CudaGraphConfig, KvCacheConfig, MoeConfig | ||
| from tensorrt_llm.llmapi import (CudaGraphConfig, Eagle3DecodingConfig, | ||
| KvCacheConfig, MoeConfig) | ||
| from tensorrt_llm.llmapi.llm_args import TorchLlmArgs | ||
| from tensorrt_llm.llmapi.llm_utils import (_resolve_kv_cache_manager_v2_auto, | ||
| apply_model_defaults_to_llm_args) | ||
| from tensorrt_llm.mapping import Mapping | ||
|
|
||
| configs = """ | ||
|
|
@@ -51,6 +55,49 @@ def test_gpt_oss_prefers_python_transceiver() -> None: | |
| assert GptOssForCausalLM.get_preferred_transceiver_runtime() == "PYTHON" | ||
|
|
||
|
|
||
| def _resolve_gpt_oss_kv_cache_manager_v2(**llm_args_kwargs) -> bool: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| """Run GPT-OSS model defaults through the same path model loading uses.""" | ||
| llm_args = TorchLlmArgs(model="/tmp/dummy_model", **llm_args_kwargs) | ||
| original_setting = llm_args.kv_cache_config.use_kv_cache_manager_v2 | ||
| model_defaults = GptOssForCausalLM.get_model_defaults(llm_args) | ||
| apply_model_defaults_to_llm_args(llm_args, model_defaults) | ||
| return _resolve_kv_cache_manager_v2_auto(llm_args, | ||
| model_defaults, | ||
| original_setting=original_setting) | ||
|
|
||
|
|
||
| def test_gpt_oss_model_defaults_select_v2(): | ||
| """GPT-OSS is VSWA, so "auto" resolves to KVCacheManagerV2.""" | ||
| assert _resolve_gpt_oss_kv_cache_manager_v2() is True | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("user_setting", [False, True]) | ||
| def test_gpt_oss_explicit_setting_wins(user_setting): | ||
| """An explicit user value is never overridden by the model default.""" | ||
| assert _resolve_gpt_oss_kv_cache_manager_v2(kv_cache_config=KvCacheConfig( | ||
| use_kv_cache_manager_v2=user_setting)) is user_setting | ||
|
|
||
|
|
||
| def test_gpt_oss_two_model_eagle3_falls_back_to_v1(): | ||
| """Two-model Eagle3 builds a separate draft engine with its own KV cache | ||
| manager, and V2 cannot split the budget between the two, so the model | ||
| default is demoted to V1.""" | ||
| assert _resolve_gpt_oss_kv_cache_manager_v2( | ||
| speculative_config=Eagle3DecodingConfig( | ||
| max_draft_len=3, | ||
| speculative_model="/tmp/dummy_eagle_model", | ||
| eagle3_one_model=False)) is False | ||
|
|
||
|
|
||
| def test_gpt_oss_one_model_eagle3_keeps_v2(): | ||
| """One-model Eagle3 shares the target engine, so V2 still applies.""" | ||
| assert _resolve_gpt_oss_kv_cache_manager_v2( | ||
| speculative_config=Eagle3DecodingConfig( | ||
| max_draft_len=3, | ||
| speculative_model="/tmp/dummy_eagle_model", | ||
| eagle3_one_model=True)) is True | ||
|
|
||
|
|
||
| def dump_config_json(dst_dir): | ||
| if os.path.exists(dst_dir): | ||
| shutil.rmtree(dst_dir) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"setting
use_kv_cache_manager_v2: trueexplicitly is not supported there" — nothing enforces this._resolve_kv_cache_manager_v2_autoreturns atsetting != "auto"before the spec-dec check, so an explicittruewith two-model Eagle3 proceeds into whatever failure mode the guard above exists to avoid, with no error and no log line. Either raise aValueErroron that combination (alongside the existing explicit-conflict errors in_util.py:150-225) or soften the doc to describe the actual behavior.