Skip to content

Commit 35c9704

Browse files
authored
[TRTLLM-12982][feat] support multi item scoring in LLM.encode (#14693)
Signed-off-by: ixlmar <206748156+ixlmar@users.noreply.github.com>
1 parent feca41c commit 35c9704

13 files changed

Lines changed: 870 additions & 137 deletions

File tree

tensorrt_llm/_torch/attention_backend/flashinfer.py

Lines changed: 275 additions & 81 deletions
Large diffs are not rendered by default.

tensorrt_llm/_torch/attention_backend/interface.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,13 @@ class AttentionForwardArgs:
748748
attention_mask_data: Optional[torch.Tensor] = None
749749
attention_sinks: Optional[torch.Tensor] = None
750750

751+
multi_item_part_lens: Optional[list[list[int]]] = None
752+
"""Additional token layout information for multi-item scoring.
753+
754+
Aggregates `TokensPrompt.multi_item_part_lens` for all requests in the batch,
755+
see `TokensPrompt` for details.
756+
"""
757+
751758
latent_cache: Optional[torch.Tensor] = None
752759
q_pe: Optional[torch.Tensor] = None
753760
mrope_rotary_cos_sin: Optional[torch.Tensor] = None

tensorrt_llm/_torch/attention_backend/star_flashinfer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ def forward(self,
322322
)
323323
assert not metadata.is_cross, "Star Attention does not support cross attention yet."
324324

325+
if forward_args.multi_item_part_lens is not None:
326+
raise ValueError(
327+
"Star Attention does not support multi-item scoring")
328+
325329
q = q.view(-1, self.num_heads, self.head_dim)
326330
k = k.view(-1, self.num_kv_heads, self.head_dim)
327331
v = v.view(-1, self.num_kv_heads, self.head_dim)

tensorrt_llm/_torch/attention_backend/trtllm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,10 @@ def forward(
17181718
)
17191719
assert not metadata.is_cross, "TRT-LLM Attention does not support cross attention yet."
17201720

1721+
if forward_args.multi_item_part_lens is not None:
1722+
raise ValueError(
1723+
"TRT-LLM Attention does not support multi-item scoring")
1724+
17211725
# SM90 forces ``use_paged_context_fmha`` on for correctness
17221726
# (https://nvbugs/5624818).
17231727
if get_sm_version() == 90:

tensorrt_llm/_torch/attention_backend/vanilla.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,11 @@ def forward(self,
382382
forward_args: Optional[AttentionForwardArgs] = None,
383383
**kwargs) -> torch.Tensor:
384384
forward_args = merge_attention_forward_args(forward_args, kwargs)
385+
386+
if forward_args.multi_item_part_lens is not None:
387+
raise ValueError(
388+
"Vanilla Attention does not support multi-item scoring")
389+
385390
if metadata.kv_cache_manager is None:
386391
# NOTE: WAR for no kv cache attn e.g. BERT,
387392
# try to separate the kv cache estimation path from no kv cache attn.

tensorrt_llm/_torch/modules/attention.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ def _attn_impl(
699699
output_sf: Optional[torch.Tensor] = None,
700700
attention_sinks: Optional[torch.Tensor] = None,
701701
has_lora: bool = False,
702+
multi_item_part_lens: Optional[list[list[int]]] = None,
702703
):
703704
num_tokens = attn_metadata.num_tokens
704705

@@ -735,6 +736,7 @@ def _attn_impl(
735736
attention_mask_data=attention_mask_data,
736737
softmax_stats_tensor=softmax_stats,
737738
attention_sinks=attention_sinks,
739+
multi_item_part_lens=multi_item_part_lens,
738740
))
739741
if isinstance(attn_output, tuple):
740742
attn_output = attn_output[0]
@@ -781,6 +783,7 @@ def _attn_impl(
781783
output=output[:num_tokens, :] if output is not None else None,
782784
output_sf=output_sf,
783785
attention_sinks=attention_sinks,
786+
multi_item_part_lens=multi_item_part_lens,
784787
))
785788
if isinstance(attn_output, tuple):
786789
assert len(
@@ -801,6 +804,7 @@ def forward_impl(
801804
mrope_config: Optional[dict],
802805
attention_sinks: Optional[torch.Tensor] = None,
803806
has_lora: bool = False,
807+
multi_item_part_lens: Optional[list[list[int]]] = None,
804808
):
805809
mrope_rotary_cos_sin = None
806810
mrope_position_deltas = None
@@ -837,17 +841,20 @@ def forward_impl(
837841
output_sf,
838842
)
839843
else:
840-
output, output_sf = self._attn_impl(q,
841-
k,
842-
v,
843-
attn_metadata,
844-
attention_mask,
845-
mrope_rotary_cos_sin,
846-
mrope_position_deltas,
847-
attention_window_size,
848-
attention_mask_data,
849-
attention_sinks=attention_sinks,
850-
has_lora=has_lora)
844+
output, output_sf = self._attn_impl(
845+
q,
846+
k,
847+
v,
848+
attn_metadata,
849+
attention_mask,
850+
mrope_rotary_cos_sin,
851+
mrope_position_deltas,
852+
attention_window_size,
853+
attention_mask_data,
854+
attention_sinks=attention_sinks,
855+
has_lora=has_lora,
856+
multi_item_part_lens=multi_item_part_lens,
857+
)
851858
if output_sf is not None:
852859
output = Fp4QuantizedTensor(output, output_sf)
853860

@@ -865,6 +872,7 @@ def forward(
865872
attention_window_size: Optional[int] = None,
866873
attention_mask_data: Optional[torch.Tensor] = None,
867874
attention_sinks: Optional[torch.Tensor] = None,
875+
multi_item_part_lens: Optional[list[list[int]]] = None,
868876
**kwargs,
869877
) -> torch.Tensor:
870878
"""
@@ -924,6 +932,23 @@ def forward(
924932
position_ids = self._adjust_position_ids_for_spec_dec(
925933
position_ids, attn_metadata)
926934

935+
if multi_item_part_lens is not None:
936+
# adjust RoPE positions for multi-item scoring
937+
current_idx = 0
938+
for req_multi_item_part_lens in multi_item_part_lens:
939+
req_prefix_len, *req_multi_item_part_lens = req_multi_item_part_lens
940+
# RoPE for prefix does not need updating and RoPE for delimiter does not matter
941+
current_idx += req_prefix_len + 1
942+
for item_len in req_multi_item_part_lens:
943+
next_idx = current_idx + item_len
944+
position_ids[0, current_idx:next_idx].copy_(
945+
torch.arange(req_prefix_len,
946+
req_prefix_len + item_len,
947+
dtype=position_ids.dtype,
948+
device=position_ids.device),
949+
non_blocking=True)
950+
current_idx = next_idx + 1 # RoPE for delimiter does not matter
951+
927952
q, k, v = self.apply_rope(q, k, v, position_ids)
928953
q, k, v = self.convert_qkv(q, k, v)
929954

@@ -932,16 +957,19 @@ def forward(
932957
f"Attention sinks are only supported with attn_backend='TRTLLM'. "
933958
f"Current backend: {self.attn_backend}.")
934959

935-
attn_output = self.forward_impl(q,
936-
k,
937-
v,
938-
attn_metadata,
939-
attention_mask,
940-
attention_window_size,
941-
attention_mask_data,
942-
mrope_config=mrope_config,
943-
attention_sinks=attention_sinks,
944-
has_lora=bool(lora_params))
960+
attn_output = self.forward_impl(
961+
q,
962+
k,
963+
v,
964+
attn_metadata,
965+
attention_mask,
966+
attention_window_size,
967+
attention_mask_data,
968+
mrope_config=mrope_config,
969+
attention_sinks=attention_sinks,
970+
has_lora=bool(lora_params),
971+
multi_item_part_lens=multi_item_part_lens,
972+
)
945973

946974
if self.attn_output_gate:
947975
gate = torch.sigmoid(gate)
@@ -1658,12 +1686,14 @@ def _get_attn_scale(position_ids: torch.Tensor) -> torch.Tensor:
16581686
q = (q * attn_scale).to(q.dtype)
16591687
return q
16601688

1661-
def forward_impl(self,
1662-
position_ids: Optional[torch.Tensor],
1663-
hidden_states: torch.Tensor,
1664-
attn_metadata: AttentionMetadata,
1665-
output: torch.Tensor,
1666-
latent_cache_gen: Optional[torch.Tensor] = None) -> None:
1689+
def forward_impl(
1690+
self,
1691+
position_ids: Optional[torch.Tensor],
1692+
hidden_states: torch.Tensor,
1693+
attn_metadata: AttentionMetadata,
1694+
output: torch.Tensor,
1695+
latent_cache_gen: Optional[torch.Tensor] = None,
1696+
) -> None:
16671697
"""
16681698
Forward pass for the MLA module. Writes result into output tensor in-place.
16691699

tensorrt_llm/_torch/pyexecutor/encoder_executor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(self, model_engine, dist):
4343

4444
self.model_engine.warmup_encoder()
4545

46-
def batch_forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
46+
def batch_forward(self, inputs: Dict[str, Any], **kwargs) -> Dict[str, torch.Tensor]:
4747
"""Execute a pre-formed batch in one forward pass.
4848
4949
Args:
@@ -54,7 +54,7 @@ def batch_forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
5454
Returns:
5555
Dict with 'logits' tensor and any other model outputs.
5656
"""
57-
return self.model_engine.encoder_forward(inputs)
57+
return self.model_engine.encoder_forward(inputs, **kwargs)
5858

5959
def shutdown(self):
6060
"""No background thread to stop — just release model engine resources."""

tensorrt_llm/_torch/pyexecutor/model_engine.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4514,7 +4514,8 @@ def _capture_encoder_cuda_graphs(self) -> None:
45144514
@torch.inference_mode()
45154515
@with_model_extra_attrs(lambda self: self.model.extra_attrs)
45164516
@nvtx_range("encoder_forward")
4517-
def encoder_forward(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
4517+
def encoder_forward(self, inputs: Dict[str, Any],
4518+
**kwargs) -> Dict[str, Any]:
45184519
"""Direct tensor-level forward for encode-only path.
45194520
45204521
Bypasses ScheduledRequests/LlmRequest entirely. Takes a raw inputs
@@ -4547,32 +4548,40 @@ def encoder_forward(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
45474548
padded_inputs,
45484549
attn_metadata=graph_attn_metadata,
45494550
padded_num_tokens=key[1] if key is not None else None)
4551+
forward_kwargs = {
4552+
"gather_ids": None,
4553+
"gather_context_logits": False,
4554+
**kwargs,
4555+
}
45504556

45514557
with with_shared_pool(
45524558
self.encoder_cuda_graph_runner.get_graph_pool()):
45534559
if key is None:
45544560
with MoeLoadBalancerIterContext(moe_load_balancer):
45554561
# Eager path — no graph for this bucket.
45564562
return self._forward_step(model_inputs,
4557-
gather_ids=None,
4558-
gather_context_logits=False)
4563+
**forward_kwargs)
45594564

45604565
if self.encoder_cuda_graph_runner.needs_capture(key):
45614566

45624567
def forward_fn(
45634568
capture_inputs: Dict[str, Any]) -> Dict[str, Any]:
4569+
capture_inputs = capture_inputs.copy()
4570+
forward_kwargs = capture_inputs.pop("_forward_kwargs")
45644571
with MoeLoadBalancerIterContext(moe_load_balancer):
4565-
return self._forward_step(
4566-
capture_inputs,
4567-
gather_ids=None,
4568-
gather_context_logits=False)
4572+
return self._forward_step(capture_inputs,
4573+
**forward_kwargs)
45694574

45704575
self.encoder_cuda_graph_runner.capture(
4571-
key, forward_fn, model_inputs)
4576+
key, forward_fn, {
4577+
**model_inputs, "_forward_kwargs": forward_kwargs
4578+
})
45724579

45734580
with MoeLoadBalancerIterContext(moe_load_balancer):
45744581
graph_outputs = self.encoder_cuda_graph_runner.replay(
4575-
key, model_inputs)
4582+
key, {
4583+
**model_inputs, "_forward_kwargs": forward_kwargs
4584+
})
45764585

45774586
# Return a clone to avoid sharing data_ptr with the static buffers.
45784587
outputs = {}

tensorrt_llm/inputs/data.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ class TokensPrompt(TypedDict):
6666
query_token_ids: NotRequired[List[int]]
6767
"""The query input token IDs for star attention."""
6868

69+
multi_item_part_lens: NotRequired[List[int]]
70+
"""Metadata for multi-item scoring.
71+
72+
Specifying this enables the multi-item scoring mode.
73+
In this mode, `prompt_token_ids` is assumed to consist of a prefix (or query) and
74+
multiple items (or candidates). Each part of the prompt is assumed to be followed
75+
by a delimiter token. The attention computation (masking and positional encodings)
76+
is altered such that the prefill logits correspond to those that would
77+
have been obtained upon running separate prefills for prefix + item1, prefix + item2,
78+
and so on. Currently, this feature is only supported in prefill-only execution
79+
via `LLM.encode`.
80+
81+
The list elements should correspond, in order, to prefix length and the length of
82+
each following item to score (all lengths in tokens).
83+
See https://github.com/flashinfer-ai/flashinfer/pull/1015 for details and nomenclature.
84+
The length should not include the delimiter tokens.
85+
"""
86+
6987

7088
PromptInputs = Union[str, List[int], TextPrompt, TokensPrompt]
7189

0 commit comments

Comments
 (0)