Signed-off-by: Acerak01-fy <wfy2003324@163.com> Signed-off-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com> Signed-off-by: zwhzzz0821 <2831474076@qq.com> Co-authored-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com> Co-authored-by: zwhzzz0821 <2831474076@qq.com>
408 lines
16 KiB
Python
408 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""
|
|
Piecewise attention for mixed causal / full (bidirectional) masks.
|
|
|
|
Dispatches each segment as a separate attention call whose causal flag
|
|
follows FlashAttention's bottom-right convention (``K[:e]`` is attended by
|
|
``Q[s:e]``, with causal alignment anchored at the bottom-right corner).
|
|
|
|
Per segment:
|
|
- causal segment ``[s, e)``: ``attn(Q[:, s:e], K[:, :e], V[:, :e], causal=True)``
|
|
- full-attn span ``[a, b)`` intersecting the query range at ``[s, e)``:
|
|
``attn(Q[:, s:e], K[:, :b], V[:, :b], causal=False)``
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Sequence
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Literal, NamedTuple
|
|
|
|
import torch
|
|
|
|
if TYPE_CHECKING:
|
|
from vllm_omni.diffusion.attention.backends.abstract import QueryRange
|
|
|
|
|
|
class Segment(NamedTuple):
|
|
q_start: int
|
|
q_end: int
|
|
kv_end: int
|
|
mode: Literal["causal", "full"]
|
|
|
|
|
|
def build_segments(full_attn_spans, query_offset, query_len):
|
|
"""
|
|
full_attn_spans: list of (start, end) half-open spans in global coordinates
|
|
query_offset: starting position of query in the global sequence
|
|
query_len: length of the query
|
|
|
|
return:
|
|
List[Segment] in global coordinates, clipped to
|
|
[query_offset, query_offset + query_len). Full-attention segments retain
|
|
the original span end as kv_end so a local query shard can attend past
|
|
its own boundary.
|
|
"""
|
|
q_start = query_offset
|
|
q_end = query_offset + query_len
|
|
|
|
segments: list[Segment] = []
|
|
cur = q_start
|
|
|
|
for span_start, span_end in full_attn_spans:
|
|
# clip span to query range
|
|
overlap_start = max(span_start, q_start)
|
|
overlap_end = min(span_end, q_end)
|
|
if overlap_start >= overlap_end:
|
|
continue
|
|
|
|
if cur < overlap_start:
|
|
segments.append(Segment(cur, overlap_start, overlap_start, "causal"))
|
|
segments.append(Segment(overlap_start, overlap_end, span_end, "full"))
|
|
cur = overlap_end
|
|
|
|
if cur < q_end:
|
|
segments.append(Segment(cur, q_end, q_end, "causal"))
|
|
|
|
return segments
|
|
|
|
|
|
def _check_homogeneous(
|
|
full_attn_spans: list[list[tuple[int, int]]],
|
|
) -> None:
|
|
"""Assert all samples share identical spans."""
|
|
if len(full_attn_spans) > 1:
|
|
ref = full_attn_spans[0]
|
|
for i, s in enumerate(full_attn_spans[1:], 1):
|
|
if s != ref:
|
|
raise ValueError(
|
|
f"piecewise_attn requires homogeneous batch: sample 0 spans {ref} != sample {i} spans {s}"
|
|
)
|
|
|
|
|
|
def piecewise_attn(
|
|
query, # (B, Sq, H, D)
|
|
key,
|
|
value,
|
|
full_attn_spans: list[list[tuple[int, int]]],
|
|
softmax_scale: float,
|
|
attn_func,
|
|
query_ranges: tuple[QueryRange, ...] | None = None,
|
|
):
|
|
_check_homogeneous(full_attn_spans)
|
|
spans = full_attn_spans[0]
|
|
ranges: tuple[tuple[int, int, int], ...]
|
|
if query_ranges is None:
|
|
query_len = query.shape[1]
|
|
ranges = ((0, query_len, key.shape[1] - query_len),)
|
|
else:
|
|
ranges = tuple((r.local_start, r.local_end, r.global_start) for r in query_ranges)
|
|
|
|
outputs = []
|
|
covered = 0
|
|
for local_start, local_end, global_start in ranges:
|
|
query_len = local_end - local_start
|
|
if local_start != covered or query_len < 0:
|
|
raise ValueError("query_ranges must cover local query contiguously")
|
|
for segment in build_segments(spans, global_start, query_len):
|
|
q_start = local_start + segment.q_start - global_start
|
|
q_end = local_start + segment.q_end - global_start
|
|
outputs.append(
|
|
attn_func(
|
|
query[:, q_start:q_end],
|
|
key[:, : segment.kv_end],
|
|
value[:, : segment.kv_end],
|
|
causal=(segment.mode == "causal"),
|
|
softmax_scale=softmax_scale,
|
|
)
|
|
)
|
|
covered = local_end
|
|
|
|
if covered != query.shape[1]:
|
|
raise ValueError("query_ranges must cover the full local query")
|
|
if not outputs:
|
|
return torch.empty_like(query)
|
|
if len(outputs) == 1:
|
|
return outputs[0]
|
|
return torch.cat(outputs, dim=1)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PagedPiecewiseSegment:
|
|
"""One aligned segment across rows in a packed paged batch."""
|
|
|
|
row_segments: tuple[Segment, ...]
|
|
query_indices: torch.Tensor
|
|
query_range: tuple[int, int] | None
|
|
# Local [start, end) range shared by every row when the batch can retain
|
|
# its original [B, S] layout. The packed query_indices remain available
|
|
# for metadata construction and the heterogeneous fallback.
|
|
local_query_range: tuple[int, int] | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PagedPiecewisePlan:
|
|
"""Piecewise segment batches for one packed paged-attention batch."""
|
|
|
|
spans: tuple[tuple[tuple[int, int], ...], ...]
|
|
segments: tuple[PagedPiecewiseSegment, ...]
|
|
num_query_tokens: int
|
|
segments_cover_query_contiguously: bool
|
|
# (batch_size, query_len) when all rows have the same local segment
|
|
# layout. Native FIA still receives flattened row-major tokens, but the
|
|
# runner can restore outputs with one batch concat instead of one indexed
|
|
# scatter per segment.
|
|
homogeneous_batch_shape: tuple[int, int] | None = None
|
|
|
|
|
|
PagedPiecewiseRunner = Callable[
|
|
[torch.Tensor, torch.Tensor | None, torch.Tensor | None, object, torch.Tensor | None],
|
|
torch.Tensor,
|
|
]
|
|
|
|
|
|
def build_paged_piecewise_plan(
|
|
full_attn_spans: Sequence[Sequence[tuple[int, int]]],
|
|
query_offsets: Sequence[int],
|
|
query_lens: Sequence[int],
|
|
seq_lens: Sequence[int],
|
|
*,
|
|
device: torch.device | str | None = None,
|
|
) -> PagedPiecewisePlan:
|
|
"""Build packed indices for corresponding piecewise segments in each row."""
|
|
|
|
row_count = len(full_attn_spans)
|
|
if row_count == 0 or not (row_count == len(query_offsets) == len(query_lens) == len(seq_lens)):
|
|
raise ValueError("Paged piecewise inputs must have one entry per row")
|
|
|
|
spans: tuple[tuple[tuple[int, int], ...], ...] = tuple(
|
|
tuple((span_start, span_end) for span_start, span_end in row_spans) for row_spans in full_attn_spans
|
|
)
|
|
|
|
packed_offsets = [0]
|
|
for row_spans, query_offset, query_len, seq_len in zip(spans, query_offsets, query_lens, seq_lens, strict=True):
|
|
previous_end = 0
|
|
for start, end in row_spans:
|
|
if start < previous_end or not 0 <= start < end <= seq_len:
|
|
raise ValueError("Paged piecewise spans must be sorted, non-overlapping, and within the sequence")
|
|
previous_end = end
|
|
if query_offset < 0 or query_len <= 0 or query_offset + query_len > seq_len:
|
|
raise ValueError("Paged piecewise query range must be within the sequence")
|
|
packed_offsets.append(packed_offsets[-1] + query_len)
|
|
|
|
segments_by_row = tuple(
|
|
tuple(build_segments(row_spans, query_offset, query_len))
|
|
for row_spans, query_offset, query_len in zip(spans, query_offsets, query_lens, strict=True)
|
|
)
|
|
if len({len(row_segments) for row_segments in segments_by_row}) != 1:
|
|
raise ValueError("Paged piecewise rows must produce aligned segments")
|
|
|
|
packed_segments = []
|
|
for row_segments in zip(*segments_by_row, strict=True):
|
|
if len({segment.mode for segment in row_segments}) != 1:
|
|
raise ValueError("Paged piecewise rows must use the same attention mode per segment")
|
|
query_indices: list[int] = []
|
|
local_ranges: list[tuple[int, int]] = []
|
|
for row_index, (segment, query_offset) in enumerate(zip(row_segments, query_offsets, strict=True)):
|
|
local_start = segment.q_start - query_offset
|
|
local_end = segment.q_end - query_offset
|
|
local_ranges.append((local_start, local_end))
|
|
query_indices.extend(range(packed_offsets[row_index] + local_start, packed_offsets[row_index] + local_end))
|
|
query_range = None
|
|
if query_indices and query_indices == list(range(query_indices[0], query_indices[-1] + 1)):
|
|
query_range = (query_indices[0], query_indices[-1] + 1)
|
|
local_query_range = local_ranges[0] if len(set(local_ranges)) == 1 else None
|
|
packed_segments.append(
|
|
PagedPiecewiseSegment(
|
|
row_segments=tuple(row_segments),
|
|
query_indices=torch.tensor(query_indices, dtype=torch.long, device=device),
|
|
query_range=query_range,
|
|
local_query_range=local_query_range,
|
|
)
|
|
)
|
|
|
|
covered = 0
|
|
for segment in packed_segments:
|
|
if segment.query_range is None or segment.query_range[0] != covered:
|
|
break
|
|
covered = segment.query_range[1]
|
|
segments_cover_query_contiguously = covered == packed_offsets[-1]
|
|
homogeneous_batch_shape: tuple[int, int] | None = None
|
|
if (
|
|
row_count > 1
|
|
and len(set(query_lens)) == 1
|
|
and all(segment.local_query_range is not None for segment in packed_segments)
|
|
):
|
|
# A common local range is sufficient even when rows have different
|
|
# global offsets or KV lengths; those values remain row-specific in
|
|
# native metadata.
|
|
homogeneous_batch_shape = (row_count, int(query_lens[0]))
|
|
return PagedPiecewisePlan(
|
|
spans=spans,
|
|
segments=tuple(packed_segments),
|
|
num_query_tokens=packed_offsets[-1],
|
|
segments_cover_query_contiguously=segments_cover_query_contiguously,
|
|
homogeneous_batch_shape=homogeneous_batch_shape,
|
|
)
|
|
|
|
|
|
def _run_homogeneous_paged_piecewise_plan(
|
|
query: torch.Tensor,
|
|
key: torch.Tensor | None,
|
|
value: torch.Tensor | None,
|
|
plan: PagedPiecewisePlan,
|
|
segment_metadata: Sequence[object],
|
|
segment_runner: PagedPiecewiseRunner,
|
|
output_buffer: torch.Tensor | None,
|
|
) -> torch.Tensor:
|
|
"""Run a homogeneous batch while retaining the old [B, S] layout.
|
|
|
|
The native FIA contract is flattened TND, so each segment is flattened
|
|
only for the kernel call. Results are reshaped back to [B, L] and
|
|
concatenated along the sequence dimension, which avoids the per-segment
|
|
``index_copy_`` used by heterogeneous packed rows.
|
|
"""
|
|
|
|
batch_shape = plan.homogeneous_batch_shape
|
|
assert batch_shape is not None
|
|
batch_size, query_len = batch_shape
|
|
if query.shape[0] != batch_size * query_len:
|
|
raise ValueError(
|
|
f"Homogeneous paged piecewise plan expects {batch_size * query_len} query tokens, got {query.shape[0]}"
|
|
)
|
|
query_rows = query.reshape(batch_size, query_len, *query.shape[1:])
|
|
key_rows = None if key is None else key.reshape(batch_size, query_len, *key.shape[1:])
|
|
value_rows = None if value is None else value.reshape(batch_size, query_len, *value.shape[1:])
|
|
|
|
segment_outputs: list[torch.Tensor] = []
|
|
for segment, metadata in zip(plan.segments, segment_metadata, strict=True):
|
|
local_range = segment.local_query_range
|
|
if local_range is None:
|
|
raise ValueError("Homogeneous paged piecewise execution requires a shared local segment range")
|
|
start, end = local_range
|
|
segment_query = query_rows[:, start:end].contiguous().reshape(-1, *query.shape[1:])
|
|
segment_key = None if key_rows is None else key_rows[:, start:end].contiguous().reshape(-1, *key.shape[1:])
|
|
segment_value = (
|
|
None if value_rows is None else value_rows[:, start:end].contiguous().reshape(-1, *value.shape[1:])
|
|
)
|
|
segment_output = segment_runner(segment_query, segment_key, segment_value, metadata, None)
|
|
expected_tokens = batch_size * (end - start)
|
|
if segment_output.shape[0] != expected_tokens:
|
|
raise ValueError(
|
|
f"Paged piecewise runner returned {segment_output.shape[0]} tokens "
|
|
f"for a {expected_tokens}-token homogeneous segment"
|
|
)
|
|
segment_outputs.append(segment_output.reshape(batch_size, end - start, *segment_output.shape[1:]))
|
|
|
|
if not segment_outputs:
|
|
result = torch.empty_like(query)
|
|
elif len(segment_outputs) == 1:
|
|
result = segment_outputs[0].reshape(plan.num_query_tokens, *segment_outputs[0].shape[2:])
|
|
else:
|
|
result_rows = torch.cat(segment_outputs, dim=1)
|
|
result = result_rows.reshape(plan.num_query_tokens, *result_rows.shape[2:])
|
|
if output_buffer is not None:
|
|
if output_buffer.shape != result.shape:
|
|
raise ValueError(
|
|
f"Paged piecewise output buffer shape {tuple(output_buffer.shape)} does not match "
|
|
f"homogeneous result shape {tuple(result.shape)}"
|
|
)
|
|
output_buffer.copy_(result)
|
|
return output_buffer
|
|
return result
|
|
|
|
|
|
def run_paged_piecewise_plan(
|
|
query: torch.Tensor,
|
|
key: torch.Tensor | None,
|
|
value: torch.Tensor | None,
|
|
plan: PagedPiecewisePlan,
|
|
segment_metadata: Sequence[object],
|
|
segment_runner: PagedPiecewiseRunner,
|
|
output_buffer: torch.Tensor | None = None,
|
|
*,
|
|
use_homogeneous_batch: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Run piecewise attention with view fast paths for contiguous segments.
|
|
|
|
``use_homogeneous_batch`` is an opt-in for native backends that can keep
|
|
identical rows in the legacy batch layout. The default remains the
|
|
indexed packed path so existing GPU/heterogeneous contracts are unchanged.
|
|
"""
|
|
|
|
if query.shape[0] != plan.num_query_tokens:
|
|
raise ValueError(f"Paged piecewise plan has {plan.num_query_tokens} query tokens, got {query.shape[0]}")
|
|
if output_buffer is not None and output_buffer.shape[0] != plan.num_query_tokens:
|
|
raise ValueError(
|
|
f"Paged piecewise output buffer has {output_buffer.shape[0]} tokens; expected {plan.num_query_tokens}"
|
|
)
|
|
|
|
if use_homogeneous_batch and plan.homogeneous_batch_shape is not None:
|
|
return _run_homogeneous_paged_piecewise_plan(
|
|
query,
|
|
key,
|
|
value,
|
|
plan,
|
|
segment_metadata,
|
|
segment_runner,
|
|
output_buffer,
|
|
)
|
|
|
|
output = output_buffer
|
|
contiguous_outputs = []
|
|
for segment, metadata in zip(plan.segments, segment_metadata, strict=True):
|
|
indices = segment.query_indices
|
|
segment_output_buffer = None
|
|
if segment.query_range is None:
|
|
segment_query = query.index_select(0, indices)
|
|
segment_key = None if key is None else key.index_select(0, indices)
|
|
segment_value = None if value is None else value.index_select(0, indices)
|
|
else:
|
|
start, end = segment.query_range
|
|
segment_query = query[start:end]
|
|
segment_key = None if key is None else key[start:end]
|
|
segment_value = None if value is None else value[start:end]
|
|
if output is not None:
|
|
segment_output_buffer = output[start:end]
|
|
segment_output = segment_runner(
|
|
segment_query,
|
|
segment_key,
|
|
segment_value,
|
|
metadata,
|
|
segment_output_buffer,
|
|
)
|
|
if segment_output.shape[0] != indices.shape[0]:
|
|
raise ValueError(
|
|
f"Paged piecewise runner returned {segment_output.shape[0]} tokens "
|
|
f"for a {indices.shape[0]}-token segment"
|
|
)
|
|
if segment_output_buffer is not None and segment_output is not segment_output_buffer:
|
|
if segment_output.shape != segment_output_buffer.shape:
|
|
raise ValueError(
|
|
f"Paged piecewise runner returned shape {tuple(segment_output.shape)} "
|
|
f"for output buffer shape {tuple(segment_output_buffer.shape)}"
|
|
)
|
|
segment_output_buffer.copy_(segment_output)
|
|
segment_output = segment_output_buffer
|
|
if plan.segments_cover_query_contiguously:
|
|
if output_buffer is None:
|
|
contiguous_outputs.append(segment_output)
|
|
continue
|
|
if output is None:
|
|
output = segment_output.new_empty((query.shape[0], *segment_output.shape[1:]))
|
|
if segment.query_range is None:
|
|
output.index_copy_(0, indices, segment_output)
|
|
else:
|
|
start, end = segment.query_range
|
|
output[start:end].copy_(segment_output)
|
|
|
|
if output_buffer is not None and plan.segments_cover_query_contiguously:
|
|
return output_buffer
|
|
if contiguous_outputs:
|
|
if len(contiguous_outputs) == 1:
|
|
return contiguous_outputs[0]
|
|
return torch.cat(contiguous_outputs, dim=0)
|
|
assert output is not None
|
|
return output
|