Decode path:
- ≤32K: paged_attention_v1 (5D KV layout, x=8) — verified on real BI-V100
- >32K: paged_attention_v2 (5D→4D permute) — verified 65K on real BI-V100
- Removes _forward_decode_pytorch Python fallback entirely
Verified: v1 passes ctx=1024..32768, v2 passes ctx=32768..65536+
flash_attn_varlen_func prefill already merged in prior commit (ad6863ed).
2287 lines
84 KiB
Python
2287 lines
84 KiB
Python
from dataclasses import dataclass
|
||
from typing import List, Optional, Tuple
|
||
import hashlib
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import re
|
||
import sys
|
||
import tempfile
|
||
import torch
|
||
import traceback
|
||
from vllm import _custom_ops as ops
|
||
from vllm.bi100_env import env_bool, env_int
|
||
from vllm.bi100_profile import bi100_profile_count, bi100_timer
|
||
|
||
try:
|
||
from vllm import corex_paged_kv_gather as _corex_paged_kv_gather
|
||
except ImportError:
|
||
_corex_paged_kv_gather = None
|
||
|
||
try:
|
||
from vllm import corex_fused_paged_prefill as _corex_fused_paged_prefill
|
||
except ImportError:
|
||
_corex_fused_paged_prefill = None
|
||
|
||
# from vllm.attention.ops.prefix_prefill import context_attention_fwd
|
||
# NOTE: context_attention_fwd (Triton kernel from prefix_prefill.py) is NOT
|
||
# imported here. On Iluvatar BI-V100 that kernel hangs the GPU card
|
||
# permanently. Chunked-prefill / prefix-caching attention is handled by
|
||
# _forward_prefix_pytorch below (pure PyTorch, no Triton dependency).
|
||
|
||
# Should be the same as PARTITION_SIZE in `paged_attention_v2_launcher`.
|
||
_PARTITION_SIZE = 512
|
||
_PYTORCH_DECODE_THRESHOLD = env_int(
|
||
"BI100_PYTORCH_DECODE_THRESHOLD", 32768, 1, 262144)
|
||
_PREFIX_BLOCKS_PER_TILE = env_int(
|
||
"BI100_PREFIX_BLOCKS_PER_TILE", 32, 1, 1024)
|
||
_FORCE_PAGED_ATTN_V2 = env_bool("BI100_FORCE_PAGED_ATTN_V2", False)
|
||
_PAGED_ATTN_DIAGNOSTICS = env_bool(
|
||
"BI100_PAGED_ATTN_DIAGNOSTICS", False)
|
||
_USE_COREX_PAGED_KV_GATHER = (
|
||
_corex_paged_kv_gather is not None
|
||
and env_bool("BI100_ATTN_COREX_PAGED_GATHER", True))
|
||
_ENABLE_COREX_FUSED_PAGED_PREFILL = env_bool(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL", False)
|
||
_FUSED_PREFILL_DIAGNOSTICS = env_bool(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_DIAGNOSTICS", False)
|
||
|
||
|
||
def _env_choice(name: str, default: str, choices: Tuple[str, ...]) -> str:
|
||
value = os.environ.get(name, default)
|
||
if value not in choices:
|
||
raise RuntimeError(
|
||
f"{name} must be one of {', '.join(choices)}, got {value!r}")
|
||
return value
|
||
|
||
|
||
_FUSED_PREFILL_SHADOW = env_bool(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW", False)
|
||
_FUSED_PREFILL_SHADOW_MAX_CALLS_PER_CONTEXT = env_int(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_MAX_CALLS_PER_CONTEXT",
|
||
2, 1, 8)
|
||
_FUSED_PREFILL_SHADOW_NUMERIC_MODE = _env_choice(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_NUMERIC_MODE",
|
||
"legacy",
|
||
("legacy", "calibrated"),
|
||
)
|
||
_FUSED_PREFILL_SHADOW_FAILURE_ACTION = _env_choice(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_FAILURE_ACTION",
|
||
"raise",
|
||
("raise", "record"),
|
||
)
|
||
_USE_COREX_FUSED_PAGED_PREFILL = (
|
||
_corex_fused_paged_prefill is not None
|
||
and _ENABLE_COREX_FUSED_PAGED_PREFILL)
|
||
_DECODE_LOG_INTERVAL = 8192 if _PAGED_ATTN_DIAGNOSTICS else 0
|
||
_DECODE_DISPATCH_LOGGED = set()
|
||
_PREFIX_DISPATCH_LOGGED = set()
|
||
_FUSED_PREFILL_DIAGNOSTICS_LOGGED = set()
|
||
_CACHE_WRITE_LOGGED = False
|
||
_FUSED_PREFILL_SHADOW_RELATIVE_L2_LIMIT = 1.0e-5
|
||
_FUSED_PREFILL_SHADOW_MAX_ABS_LIMIT = 1.0e-3
|
||
_FUSED_PREFILL_SHADOW_ERROR_MULTIPLIER = 2.0
|
||
_FUSED_PREFILL_SHADOW_RATIO_FLOOR = 1.0e-12
|
||
_FUSED_PREFILL_SHADOW_STATE = {
|
||
"pid": None,
|
||
"records": [],
|
||
}
|
||
_ACTIVATION_CAPTURE_ENABLED = env_bool(
|
||
"BI100_ATTN_CAPTURE_REPLAY", False)
|
||
_ACTIVATION_CAPTURE_ATTESTATION = (
|
||
"synthetic-exact-prompt-v1")
|
||
_ACTIVATION_CAPTURE_STATE = {
|
||
"pid": None,
|
||
"seen_by_bucket": {},
|
||
"records": [],
|
||
}
|
||
|
||
|
||
def _parse_fused_prefill_shadow_contexts(raw: str) -> Tuple[int, ...]:
|
||
"""Parse fixed lower-bound buckets used by the diagnostic shadow."""
|
||
values = []
|
||
for field in raw.split(","):
|
||
field = field.strip()
|
||
if not field:
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_CONTEXTS "
|
||
"contains an empty field")
|
||
try:
|
||
value = int(field)
|
||
except ValueError as exc:
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_CONTEXTS must "
|
||
"contain integers") from exc
|
||
if value < 0 or value > 262144:
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_CONTEXTS value "
|
||
"is outside [0, 262144]")
|
||
values.append(value)
|
||
if not values or values != sorted(set(values)):
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_CONTEXTS must be "
|
||
"strictly increasing and unique")
|
||
return tuple(values)
|
||
|
||
|
||
_FUSED_PREFILL_SHADOW_CONTEXTS = _parse_fused_prefill_shadow_contexts(
|
||
os.environ.get(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_CONTEXTS",
|
||
"49152,114688"))
|
||
|
||
|
||
def _validate_fused_prefill_shadow_configuration(
|
||
enabled: bool,
|
||
fused_enabled: bool,
|
||
report_dir: Optional[str],
|
||
run_id: Optional[str],
|
||
) -> Optional[Path]:
|
||
"""Validate that the diagnostic cannot silently write ambiguous data."""
|
||
if not enabled:
|
||
return None
|
||
if not fused_enabled:
|
||
raise RuntimeError(
|
||
"fused-prefill shadow requires the fused-prefill path")
|
||
if (
|
||
_FUSED_PREFILL_SHADOW_FAILURE_ACTION == "record"
|
||
and _FUSED_PREFILL_SHADOW_NUMERIC_MODE != "calibrated"
|
||
):
|
||
raise RuntimeError(
|
||
"record-only shadow failures require calibrated numeric mode")
|
||
if not report_dir:
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_REPORT_DIR is required")
|
||
path = Path(report_dir).expanduser()
|
||
tmp_root = Path("/tmp").resolve()
|
||
try:
|
||
path = path.resolve(strict=False)
|
||
except OSError as exc:
|
||
raise RuntimeError(
|
||
"fused-prefill shadow report directory cannot be resolved") \
|
||
from exc
|
||
if (
|
||
not path.is_absolute()
|
||
or path == tmp_root
|
||
or not path.is_relative_to(tmp_root)
|
||
):
|
||
raise RuntimeError(
|
||
"fused-prefill shadow report directory must be under /tmp")
|
||
if (
|
||
not run_id
|
||
or len(run_id) > 96
|
||
or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", run_id) is None
|
||
):
|
||
raise RuntimeError(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_RUN_ID is invalid")
|
||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||
if not path.resolve(strict=True).is_relative_to(tmp_root):
|
||
raise RuntimeError(
|
||
"fused-prefill shadow report directory escaped /tmp")
|
||
try:
|
||
path.chmod(0o700)
|
||
except OSError:
|
||
pass
|
||
return path
|
||
|
||
|
||
_FUSED_PREFILL_SHADOW_RUN_ID = os.environ.get(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_RUN_ID")
|
||
_FUSED_PREFILL_SHADOW_REPORT_DIR = (
|
||
_validate_fused_prefill_shadow_configuration(
|
||
_FUSED_PREFILL_SHADOW,
|
||
_USE_COREX_FUSED_PAGED_PREFILL,
|
||
os.environ.get(
|
||
"BI100_ATTN_COREX_FUSED_PREFILL_SHADOW_REPORT_DIR"),
|
||
_FUSED_PREFILL_SHADOW_RUN_ID,
|
||
))
|
||
|
||
|
||
def _parse_strict_int_tuple(
|
||
raw: str,
|
||
*,
|
||
name: str,
|
||
minimum: int,
|
||
maximum: int,
|
||
) -> Tuple[int, ...]:
|
||
values = []
|
||
for field in raw.split(","):
|
||
field = field.strip()
|
||
if not field:
|
||
raise RuntimeError(f"{name} contains an empty field")
|
||
try:
|
||
value = int(field)
|
||
except ValueError as exc:
|
||
raise RuntimeError(f"{name} must contain integers") from exc
|
||
if value < minimum or value > maximum:
|
||
raise RuntimeError(
|
||
f"{name} value is outside [{minimum}, {maximum}]")
|
||
values.append(value)
|
||
if not values or values != sorted(set(values)):
|
||
raise RuntimeError(f"{name} must be strictly increasing and unique")
|
||
return tuple(values)
|
||
|
||
|
||
_ACTIVATION_CAPTURE_CONTEXTS = _parse_strict_int_tuple(
|
||
os.environ.get(
|
||
"BI100_ATTN_CAPTURE_REPLAY_CONTEXTS",
|
||
"24576,57344,122880",
|
||
),
|
||
name="BI100_ATTN_CAPTURE_REPLAY_CONTEXTS",
|
||
minimum=0,
|
||
maximum=262144,
|
||
)
|
||
_ACTIVATION_CAPTURE_CALL_ORDINALS = _parse_strict_int_tuple(
|
||
os.environ.get(
|
||
"BI100_ATTN_CAPTURE_REPLAY_CALL_ORDINALS",
|
||
"0,4,9",
|
||
),
|
||
name="BI100_ATTN_CAPTURE_REPLAY_CALL_ORDINALS",
|
||
minimum=0,
|
||
maximum=63,
|
||
)
|
||
|
||
|
||
def _validate_activation_capture_configuration(
|
||
enabled: bool,
|
||
fused_enabled: bool,
|
||
report_dir: Optional[str],
|
||
run_id: Optional[str],
|
||
source_revision: Optional[str],
|
||
runtime_identity: Optional[str],
|
||
attestation: Optional[str],
|
||
) -> Optional[Path]:
|
||
if not enabled:
|
||
return None
|
||
if fused_enabled:
|
||
raise RuntimeError(
|
||
"activation capture requires the baseline PyTorch fallback")
|
||
if attestation != _ACTIVATION_CAPTURE_ATTESTATION:
|
||
raise RuntimeError(
|
||
"activation capture requires the synthetic prompt attestation")
|
||
if (
|
||
not source_revision
|
||
or re.fullmatch(r"[0-9a-f]{40,64}", source_revision) is None
|
||
):
|
||
raise RuntimeError(
|
||
"BI100_ATTN_CAPTURE_REPLAY_SOURCE_REVISION is invalid")
|
||
if (
|
||
not runtime_identity
|
||
or len(runtime_identity) > 160
|
||
or re.fullmatch(
|
||
r"[A-Za-z0-9][A-Za-z0-9_.:/-]*", runtime_identity) is None
|
||
):
|
||
raise RuntimeError(
|
||
"BI100_ATTN_CAPTURE_REPLAY_RUNTIME_IDENTITY is invalid")
|
||
if (
|
||
not run_id
|
||
or len(run_id) > 96
|
||
or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", run_id) is None
|
||
):
|
||
raise RuntimeError(
|
||
"BI100_ATTN_CAPTURE_REPLAY_RUN_ID is invalid")
|
||
if not report_dir:
|
||
raise RuntimeError(
|
||
"BI100_ATTN_CAPTURE_REPLAY_DIR is required")
|
||
path = Path(report_dir).expanduser()
|
||
tmp_root = Path("/tmp").resolve()
|
||
try:
|
||
path = path.resolve(strict=False)
|
||
except OSError as exc:
|
||
raise RuntimeError(
|
||
"activation capture directory cannot be resolved") from exc
|
||
if (
|
||
not path.is_absolute()
|
||
or path == tmp_root
|
||
or not path.is_relative_to(tmp_root)
|
||
):
|
||
raise RuntimeError(
|
||
"activation capture directory must be under /tmp")
|
||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||
if not path.resolve(strict=True).is_relative_to(tmp_root):
|
||
raise RuntimeError(
|
||
"activation capture directory escaped /tmp")
|
||
try:
|
||
path.chmod(0o700)
|
||
except OSError:
|
||
pass
|
||
return path
|
||
|
||
|
||
_ACTIVATION_CAPTURE_RUN_ID = os.environ.get(
|
||
"BI100_ATTN_CAPTURE_REPLAY_RUN_ID")
|
||
_ACTIVATION_CAPTURE_SOURCE_REVISION = os.environ.get(
|
||
"BI100_ATTN_CAPTURE_REPLAY_SOURCE_REVISION")
|
||
_ACTIVATION_CAPTURE_RUNTIME_IDENTITY = os.environ.get(
|
||
"BI100_ATTN_CAPTURE_REPLAY_RUNTIME_IDENTITY")
|
||
_ACTIVATION_CAPTURE_DIR = _validate_activation_capture_configuration(
|
||
_ACTIVATION_CAPTURE_ENABLED,
|
||
_ENABLE_COREX_FUSED_PAGED_PREFILL,
|
||
os.environ.get("BI100_ATTN_CAPTURE_REPLAY_DIR"),
|
||
_ACTIVATION_CAPTURE_RUN_ID,
|
||
_ACTIVATION_CAPTURE_SOURCE_REVISION,
|
||
_ACTIVATION_CAPTURE_RUNTIME_IDENTITY,
|
||
os.environ.get("BI100_ATTN_CAPTURE_REPLAY_SYNTHETIC_ATTESTATION"),
|
||
)
|
||
|
||
|
||
def _log_corex_fused_prefill_diagnostic(stage: str, **fields) -> None:
|
||
"""Emit one privacy-safe guard snapshot per stage and worker."""
|
||
if not _FUSED_PREFILL_DIAGNOSTICS:
|
||
return
|
||
key = (os.getpid(), stage)
|
||
if key in _FUSED_PREFILL_DIAGNOSTICS_LOGGED:
|
||
return
|
||
details = " ".join(f"{name}={value}" for name, value in fields.items())
|
||
print(
|
||
"[BI100 PAGED_ATTN] fused_prefill_guard "
|
||
f"pid={os.getpid()} rank={os.environ.get('RANK', '?')} "
|
||
f"local_rank={os.environ.get('LOCAL_RANK', '?')} stage={stage} "
|
||
f"{details}",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
_FUSED_PREFILL_DIAGNOSTICS_LOGGED.add(key)
|
||
|
||
|
||
def _fused_prefill_shadow_rank() -> int:
|
||
distributed = getattr(torch, "distributed", None)
|
||
if distributed is not None:
|
||
try:
|
||
if (
|
||
distributed.is_available()
|
||
and distributed.is_initialized()
|
||
):
|
||
rank = int(distributed.get_rank())
|
||
if rank >= 0:
|
||
return rank
|
||
except (AttributeError, RuntimeError, TypeError, ValueError):
|
||
pass
|
||
for name in ("RANK", "LOCAL_RANK"):
|
||
raw = os.environ.get(name)
|
||
if raw is None:
|
||
continue
|
||
try:
|
||
value = int(raw)
|
||
except ValueError:
|
||
continue
|
||
if value >= 0:
|
||
return value
|
||
cuda = getattr(torch, "cuda", None)
|
||
if cuda is not None:
|
||
try:
|
||
if cuda.is_available():
|
||
device = int(cuda.current_device())
|
||
if device >= 0:
|
||
return device
|
||
except (AttributeError, RuntimeError, TypeError, ValueError):
|
||
pass
|
||
return -1
|
||
|
||
|
||
def _activation_capture_process_state() -> dict:
|
||
pid = os.getpid()
|
||
if _ACTIVATION_CAPTURE_STATE["pid"] != pid:
|
||
_ACTIVATION_CAPTURE_STATE["pid"] = pid
|
||
_ACTIVATION_CAPTURE_STATE["seen_by_bucket"] = {}
|
||
_ACTIVATION_CAPTURE_STATE["records"] = []
|
||
return _ACTIVATION_CAPTURE_STATE
|
||
|
||
|
||
def _activation_capture_bucket(context_tokens: int) -> Optional[int]:
|
||
for index, lower_bound in enumerate(_ACTIVATION_CAPTURE_CONTEXTS):
|
||
upper_bound = (
|
||
_ACTIVATION_CAPTURE_CONTEXTS[index + 1]
|
||
if index + 1 < len(_ACTIVATION_CAPTURE_CONTEXTS)
|
||
else 262145
|
||
)
|
||
if lower_bound <= context_tokens < upper_bound:
|
||
return lower_bound
|
||
return None
|
||
|
||
|
||
def _atomic_write_activation_manifest(records: list) -> None:
|
||
if _ACTIVATION_CAPTURE_DIR is None:
|
||
raise RuntimeError("activation capture directory is unset")
|
||
rank = _fused_prefill_shadow_rank()
|
||
if rank < 0:
|
||
raise RuntimeError("activation capture cannot determine TP rank")
|
||
value = {
|
||
"schema": "bi100-fused-prefill-activation-bank-v1",
|
||
"version": 1,
|
||
"run_id": _ACTIVATION_CAPTURE_RUN_ID,
|
||
"rank": rank,
|
||
"source_revision": _ACTIVATION_CAPTURE_SOURCE_REVISION,
|
||
"runtime_identity": _ACTIVATION_CAPTURE_RUNTIME_IDENTITY,
|
||
"producer": "baseline-pytorch-fallback",
|
||
"synthetic_prompt_attestation": (
|
||
_ACTIVATION_CAPTURE_ATTESTATION),
|
||
"selection": {
|
||
"context_buckets": list(_ACTIVATION_CAPTURE_CONTEXTS),
|
||
"full_attention_call_ordinals": list(
|
||
_ACTIVATION_CAPTURE_CALL_ORDINALS),
|
||
},
|
||
"record_count": len(records),
|
||
"records": records,
|
||
"privacy": {
|
||
"raw_activation_files_private": True,
|
||
"raw_activation_files_may_be_committed": False,
|
||
"contains_prompts": False,
|
||
"contains_model_outputs": False,
|
||
"contains_token_ids": False,
|
||
"contains_credentials": False,
|
||
},
|
||
}
|
||
destination = _ACTIVATION_CAPTURE_DIR / f"rank-{rank}.manifest.json"
|
||
descriptor, temporary = tempfile.mkstemp(
|
||
prefix=f".{destination.name}.",
|
||
suffix=".tmp",
|
||
dir=destination.parent,
|
||
)
|
||
try:
|
||
with os.fdopen(descriptor, "w", encoding="ascii") as stream:
|
||
json.dump(value, stream, ensure_ascii=True, indent=2,
|
||
sort_keys=True)
|
||
stream.write("\n")
|
||
stream.flush()
|
||
os.fsync(stream.fileno())
|
||
os.replace(temporary, destination)
|
||
finally:
|
||
try:
|
||
os.unlink(temporary)
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
|
||
def _reserve_activation_capture(
|
||
context_tokens: int,
|
||
) -> Optional[Tuple[int, int]]:
|
||
if not _ACTIVATION_CAPTURE_ENABLED:
|
||
return None
|
||
bucket = _activation_capture_bucket(context_tokens)
|
||
if bucket is None:
|
||
return None
|
||
state = _activation_capture_process_state()
|
||
ordinal = int(state["seen_by_bucket"].get(bucket, 0))
|
||
state["seen_by_bucket"][bucket] = ordinal + 1
|
||
if ordinal not in _ACTIVATION_CAPTURE_CALL_ORDINALS:
|
||
return None
|
||
return bucket, ordinal
|
||
|
||
|
||
def _tensor_shape_dtype(tensor: torch.Tensor) -> dict:
|
||
return {
|
||
"shape": list(tensor.shape),
|
||
"dtype": str(tensor.dtype),
|
||
}
|
||
|
||
|
||
def _sha256_file(path: Path) -> str:
|
||
digest = hashlib.sha256()
|
||
with path.open("rb") as stream:
|
||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||
digest.update(chunk)
|
||
return digest.hexdigest()
|
||
|
||
|
||
def _capture_fused_prefill_activation(
|
||
reservation: Tuple[int, int],
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
active_block_table: torch.Tensor,
|
||
context_tokens: int,
|
||
scale: float,
|
||
) -> None:
|
||
if _ACTIVATION_CAPTURE_DIR is None:
|
||
raise RuntimeError("activation capture directory is unset")
|
||
bucket, ordinal = reservation
|
||
rank = _fused_prefill_shadow_rank()
|
||
if rank < 0:
|
||
raise RuntimeError("activation capture cannot determine TP rank")
|
||
|
||
active_ids = [
|
||
int(value) for value in active_block_table.detach().cpu().tolist()
|
||
]
|
||
identity_to_compact = {}
|
||
unique_ids = []
|
||
compact_table = []
|
||
for physical_id in active_ids:
|
||
if physical_id < 0 or physical_id >= key_cache.shape[0]:
|
||
raise RuntimeError(
|
||
"activation capture block table is outside the KV cache")
|
||
compact_id = identity_to_compact.get(physical_id)
|
||
if compact_id is None:
|
||
compact_id = len(unique_ids)
|
||
identity_to_compact[physical_id] = compact_id
|
||
unique_ids.append(physical_id)
|
||
compact_table.append(compact_id)
|
||
|
||
if unique_ids:
|
||
physical = torch.tensor(
|
||
unique_ids,
|
||
dtype=torch.long,
|
||
device=key_cache.device,
|
||
)
|
||
compact_key_cache = (
|
||
key_cache.index_select(0, physical).detach().cpu().contiguous())
|
||
compact_value_cache = (
|
||
value_cache.index_select(0, physical).detach().cpu().contiguous())
|
||
else:
|
||
compact_key_cache = key_cache[:0].detach().cpu().contiguous()
|
||
compact_value_cache = value_cache[:0].detach().cpu().contiguous()
|
||
compact_block_table = torch.tensor(
|
||
compact_table,
|
||
dtype=torch.int32,
|
||
)
|
||
tensors = {
|
||
"query": query.detach().cpu().contiguous(),
|
||
"key": key.detach().cpu().contiguous(),
|
||
"value": value.detach().cpu().contiguous(),
|
||
"key_cache": compact_key_cache,
|
||
"value_cache": compact_value_cache,
|
||
"block_table": compact_block_table,
|
||
}
|
||
filename = (
|
||
f"rank-{rank}.bucket-{bucket}.ordinal-{ordinal}."
|
||
f"ctx-{context_tokens}.q-{query.shape[0]}.pt"
|
||
)
|
||
destination = _ACTIVATION_CAPTURE_DIR / filename
|
||
descriptor, temporary = tempfile.mkstemp(
|
||
prefix=f".{filename}.", suffix=".tmp",
|
||
dir=destination.parent)
|
||
os.close(descriptor)
|
||
try:
|
||
torch.save({
|
||
"schema": "bi100-fused-prefill-activation-case-v1",
|
||
"version": 1,
|
||
"context_tokens": context_tokens,
|
||
"scale": float(scale),
|
||
"rank": rank,
|
||
"bucket": bucket,
|
||
"call_ordinal": ordinal,
|
||
"tensors": tensors,
|
||
}, temporary)
|
||
os.chmod(temporary, 0o600)
|
||
with open(temporary, "rb") as stream:
|
||
os.fsync(stream.fileno())
|
||
os.replace(temporary, destination)
|
||
finally:
|
||
try:
|
||
os.unlink(temporary)
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
state = _activation_capture_process_state()
|
||
state["records"].append({
|
||
"bucket_min_context_tokens": bucket,
|
||
"call_ordinal": ordinal,
|
||
"context_tokens": context_tokens,
|
||
"query_length": int(query.shape[0]),
|
||
"file": filename,
|
||
"sha256": _sha256_file(destination),
|
||
"size_bytes": destination.stat().st_size,
|
||
"compact_physical_blocks": len(unique_ids),
|
||
"logical_blocks": len(compact_table),
|
||
"tensors": {
|
||
name: _tensor_shape_dtype(tensor)
|
||
for name, tensor in tensors.items()
|
||
},
|
||
})
|
||
_atomic_write_activation_manifest(state["records"])
|
||
|
||
|
||
def _fused_prefill_shadow_process_state() -> dict:
|
||
pid = os.getpid()
|
||
if _FUSED_PREFILL_SHADOW_STATE["pid"] != pid:
|
||
_FUSED_PREFILL_SHADOW_STATE["pid"] = pid
|
||
_FUSED_PREFILL_SHADOW_STATE["records"] = []
|
||
return _FUSED_PREFILL_SHADOW_STATE
|
||
|
||
|
||
def _fused_prefill_shadow_report_path() -> Path:
|
||
if _FUSED_PREFILL_SHADOW_REPORT_DIR is None:
|
||
raise RuntimeError("fused-prefill shadow report directory is unset")
|
||
rank = _fused_prefill_shadow_rank()
|
||
rank_label = str(rank) if rank >= 0 else "unknown"
|
||
return _FUSED_PREFILL_SHADOW_REPORT_DIR / (
|
||
f"rank-{rank_label}-pid-{os.getpid()}.json")
|
||
|
||
|
||
def _atomic_write_fused_prefill_shadow_report(value: dict) -> None:
|
||
path = _fused_prefill_shadow_report_path()
|
||
descriptor, temporary = tempfile.mkstemp(
|
||
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
||
try:
|
||
os.fchmod(descriptor, 0o600)
|
||
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
||
json.dump(value, stream, ensure_ascii=True, indent=2,
|
||
sort_keys=True)
|
||
stream.write("\n")
|
||
stream.flush()
|
||
os.fsync(stream.fileno())
|
||
os.replace(temporary, path)
|
||
finally:
|
||
try:
|
||
os.unlink(temporary)
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
|
||
def _build_fused_prefill_shadow_report(records: list) -> dict:
|
||
expected = (
|
||
len(_FUSED_PREFILL_SHADOW_CONTEXTS)
|
||
* _FUSED_PREFILL_SHADOW_MAX_CALLS_PER_CONTEXT)
|
||
completed = [
|
||
record for record in records
|
||
if record["status"] in {"pass", "fail", "invalid"}
|
||
]
|
||
failures = [record for record in completed if record["status"] == "fail"]
|
||
invalid = [record for record in completed if record["status"] == "invalid"]
|
||
pending = [record for record in records if record["status"] == "pending"]
|
||
relative_l2_values = [
|
||
record["relative_l2"] for record in completed
|
||
if isinstance(record.get("relative_l2"), float)
|
||
]
|
||
max_abs_values = [
|
||
record["max_abs"] for record in completed
|
||
if isinstance(record.get("max_abs"), float)
|
||
]
|
||
if invalid:
|
||
status = "invalid"
|
||
elif failures:
|
||
status = "fail"
|
||
elif len(completed) == expected and not pending:
|
||
status = "pass"
|
||
else:
|
||
status = "collecting"
|
||
report = {
|
||
"schema": "bi100-fused-prefill-real-activation-shadow-v1",
|
||
"version": 1,
|
||
"run_id": _FUSED_PREFILL_SHADOW_RUN_ID,
|
||
"pid": os.getpid(),
|
||
"rank": _fused_prefill_shadow_rank(),
|
||
"status": status,
|
||
"selection": {
|
||
"minimum_context_tokens": list(
|
||
_FUSED_PREFILL_SHADOW_CONTEXTS),
|
||
"max_calls_per_context": (
|
||
_FUSED_PREFILL_SHADOW_MAX_CALLS_PER_CONTEXT),
|
||
},
|
||
"thresholds": {
|
||
"require_finite_candidate": True,
|
||
"require_finite_reference": True,
|
||
"maximum_relative_l2": (
|
||
_FUSED_PREFILL_SHADOW_RELATIVE_L2_LIMIT),
|
||
"maximum_absolute_error": (
|
||
_FUSED_PREFILL_SHADOW_MAX_ABS_LIMIT),
|
||
},
|
||
"observations": {
|
||
"expected": expected,
|
||
"reserved": len(records),
|
||
"completed": len(completed),
|
||
"passed": sum(record["status"] == "pass" for record in records),
|
||
"failed": len(failures),
|
||
"invalid": len(invalid),
|
||
"pending": len(pending),
|
||
"maximum_relative_l2": (
|
||
max(relative_l2_values) if relative_l2_values else None),
|
||
"maximum_absolute_error": (
|
||
max(max_abs_values) if max_abs_values else None),
|
||
},
|
||
"records": records,
|
||
"privacy": {
|
||
"contains_prompts": False,
|
||
"contains_model_outputs": False,
|
||
"contains_tensor_values": False,
|
||
"contains_token_ids": False,
|
||
"contains_credentials": False,
|
||
},
|
||
}
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
report["schema"] = (
|
||
"bi100-fused-prefill-real-activation-calibrated-shadow-v1")
|
||
report["thresholds"] = {
|
||
"require_finite_candidate": True,
|
||
"require_finite_reference": True,
|
||
"maximum_candidate_vs_rounded_relative_l2": (
|
||
_FUSED_PREFILL_SHADOW_RELATIVE_L2_LIMIT),
|
||
"maximum_error_multiple_over_fp16_rounding": (
|
||
_FUSED_PREFILL_SHADOW_ERROR_MULTIPLIER),
|
||
"ratio_denominator_floor": (
|
||
_FUSED_PREFILL_SHADOW_RATIO_FLOOR),
|
||
"fixed_max_abs_role": "diagnostic_only",
|
||
"finite_failure_action": (
|
||
_FUSED_PREFILL_SHADOW_FAILURE_ACTION),
|
||
}
|
||
calibrated_metrics = (
|
||
"candidate_to_fp32_relative_l2",
|
||
"candidate_to_fp32_max_abs",
|
||
"rounded_to_fp32_relative_l2",
|
||
"rounded_to_fp32_max_abs",
|
||
"relative_l2_baseline_ratio",
|
||
"max_abs_baseline_ratio",
|
||
)
|
||
for name in calibrated_metrics:
|
||
values = [
|
||
record[name] for record in completed
|
||
if isinstance(record.get(name), float)
|
||
]
|
||
report["observations"][f"maximum_{name}"] = (
|
||
max(values) if values else None)
|
||
return report
|
||
|
||
|
||
def _reserve_fused_prefill_shadow(
|
||
query: torch.Tensor,
|
||
block_context_len: int,
|
||
num_q_heads: int,
|
||
num_kv_heads: int,
|
||
head_dim: int,
|
||
block_size: int,
|
||
) -> Optional[int]:
|
||
if not _FUSED_PREFILL_SHADOW:
|
||
return None
|
||
state = _fused_prefill_shadow_process_state()
|
||
records = state["records"]
|
||
selected_bucket = None
|
||
for bucket_index, bucket in enumerate(_FUSED_PREFILL_SHADOW_CONTEXTS):
|
||
upper_bound = (
|
||
_FUSED_PREFILL_SHADOW_CONTEXTS[bucket_index + 1]
|
||
if bucket_index + 1 < len(_FUSED_PREFILL_SHADOW_CONTEXTS)
|
||
else None)
|
||
used = sum(
|
||
record["bucket_min_context_tokens"] == bucket
|
||
for record in records)
|
||
if (
|
||
block_context_len >= bucket
|
||
and (upper_bound is None or block_context_len < upper_bound)
|
||
and used < _FUSED_PREFILL_SHADOW_MAX_CALLS_PER_CONTEXT
|
||
):
|
||
selected_bucket = bucket
|
||
break
|
||
if selected_bucket is None:
|
||
return None
|
||
record = {
|
||
"index": len(records),
|
||
"status": "pending",
|
||
"bucket_min_context_tokens": selected_bucket,
|
||
"context_tokens": block_context_len,
|
||
"query_shape": list(query.shape),
|
||
"query_heads": num_q_heads,
|
||
"kv_heads": num_kv_heads,
|
||
"head_dim": head_dim,
|
||
"block_size": block_size,
|
||
"candidate_finite": None,
|
||
"reference_finite": None,
|
||
"relative_l2": None,
|
||
"max_abs": None,
|
||
"error_stage": None,
|
||
"error_type": None,
|
||
}
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
record.update({
|
||
"candidate_to_fp32_relative_l2": None,
|
||
"candidate_to_fp32_max_abs": None,
|
||
"rounded_to_fp32_relative_l2": None,
|
||
"rounded_to_fp32_max_abs": None,
|
||
"relative_l2_baseline_ratio": None,
|
||
"max_abs_baseline_ratio": None,
|
||
})
|
||
records.append(record)
|
||
_atomic_write_fused_prefill_shadow_report(
|
||
_build_fused_prefill_shadow_report(records))
|
||
return record["index"]
|
||
|
||
|
||
def _finish_fused_prefill_shadow(
|
||
index: int,
|
||
*,
|
||
status: str,
|
||
candidate_finite: Optional[bool] = None,
|
||
reference_finite: Optional[bool] = None,
|
||
relative_l2: Optional[float] = None,
|
||
max_abs: Optional[float] = None,
|
||
candidate_to_fp32_relative_l2: Optional[float] = None,
|
||
candidate_to_fp32_max_abs: Optional[float] = None,
|
||
rounded_to_fp32_relative_l2: Optional[float] = None,
|
||
rounded_to_fp32_max_abs: Optional[float] = None,
|
||
relative_l2_baseline_ratio: Optional[float] = None,
|
||
max_abs_baseline_ratio: Optional[float] = None,
|
||
error_stage: Optional[str] = None,
|
||
error_type: Optional[str] = None,
|
||
) -> None:
|
||
state = _fused_prefill_shadow_process_state()
|
||
records = state["records"]
|
||
if index < 0 or index >= len(records):
|
||
raise RuntimeError("fused-prefill shadow record index is invalid")
|
||
if status not in {"pass", "fail", "invalid"}:
|
||
raise RuntimeError("fused-prefill shadow status is invalid")
|
||
record = records[index]
|
||
updates = {
|
||
"status": status,
|
||
"candidate_finite": candidate_finite,
|
||
"reference_finite": reference_finite,
|
||
"relative_l2": relative_l2,
|
||
"max_abs": max_abs,
|
||
"error_stage": error_stage,
|
||
"error_type": error_type,
|
||
}
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
updates.update({
|
||
"candidate_to_fp32_relative_l2": (
|
||
candidate_to_fp32_relative_l2),
|
||
"candidate_to_fp32_max_abs": candidate_to_fp32_max_abs,
|
||
"rounded_to_fp32_relative_l2": (
|
||
rounded_to_fp32_relative_l2),
|
||
"rounded_to_fp32_max_abs": rounded_to_fp32_max_abs,
|
||
"relative_l2_baseline_ratio": relative_l2_baseline_ratio,
|
||
"max_abs_baseline_ratio": max_abs_baseline_ratio,
|
||
})
|
||
record.update(updates)
|
||
_atomic_write_fused_prefill_shadow_report(
|
||
_build_fused_prefill_shadow_report(records))
|
||
|
||
|
||
def _calibrated_shadow_metrics_qualified(metrics: dict) -> bool:
|
||
return (
|
||
metrics["relative_l2"]
|
||
<= _FUSED_PREFILL_SHADOW_RELATIVE_L2_LIMIT
|
||
and metrics["candidate_to_fp32_relative_l2"]
|
||
<= (
|
||
_FUSED_PREFILL_SHADOW_ERROR_MULTIPLIER
|
||
* metrics["rounded_to_fp32_relative_l2"]
|
||
+ _FUSED_PREFILL_SHADOW_RATIO_FLOOR
|
||
)
|
||
and metrics["candidate_to_fp32_max_abs"]
|
||
<= (
|
||
_FUSED_PREFILL_SHADOW_ERROR_MULTIPLIER
|
||
* metrics["rounded_to_fp32_max_abs"]
|
||
+ _FUSED_PREFILL_SHADOW_RATIO_FLOOR
|
||
)
|
||
)
|
||
|
||
|
||
def _error_metrics(
|
||
actual: torch.Tensor,
|
||
reference: torch.Tensor,
|
||
denominator: float,
|
||
) -> Tuple[float, float]:
|
||
difference = actual - reference
|
||
relative_l2 = float(torch.norm(difference).item()) / denominator
|
||
max_abs = float(difference.abs().max().item())
|
||
return relative_l2, max_abs
|
||
|
||
|
||
def _compare_fused_prefill_shadow_outputs(
|
||
candidate: torch.Tensor,
|
||
reference: torch.Tensor,
|
||
reference_fp32: Optional[torch.Tensor] = None,
|
||
) -> dict:
|
||
candidate_float = candidate.float()
|
||
reference_float = reference.float()
|
||
candidate_finite = bool(torch.isfinite(candidate_float).all().item())
|
||
reference_finite = bool(torch.isfinite(reference_float).all().item())
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
if reference_fp32 is None:
|
||
raise RuntimeError(
|
||
"calibrated fused-prefill shadow requires FP32 reference")
|
||
reference_fp32 = reference_fp32.float()
|
||
reference_finite = (
|
||
reference_finite
|
||
and bool(torch.isfinite(reference_fp32).all().item())
|
||
)
|
||
if not candidate_finite or not reference_finite:
|
||
result = {
|
||
"status": "fail" if not candidate_finite else "invalid",
|
||
"candidate_finite": candidate_finite,
|
||
"reference_finite": reference_finite,
|
||
"relative_l2": None,
|
||
"max_abs": None,
|
||
}
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
result.update({
|
||
"candidate_to_fp32_relative_l2": None,
|
||
"candidate_to_fp32_max_abs": None,
|
||
"rounded_to_fp32_relative_l2": None,
|
||
"rounded_to_fp32_max_abs": None,
|
||
"relative_l2_baseline_ratio": None,
|
||
"max_abs_baseline_ratio": None,
|
||
})
|
||
return result
|
||
denominator = max(float(torch.norm(reference_float).item()), 1.0e-12)
|
||
relative_l2, max_abs = _error_metrics(
|
||
candidate_float, reference_float, denominator)
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE == "calibrated":
|
||
if reference_fp32 is None:
|
||
raise RuntimeError(
|
||
"calibrated fused-prefill shadow lost its FP32 reference")
|
||
fp32_denominator = max(
|
||
float(torch.norm(reference_fp32).item()),
|
||
_FUSED_PREFILL_SHADOW_RATIO_FLOOR,
|
||
)
|
||
candidate_fp32_relative_l2, candidate_fp32_max_abs = (
|
||
_error_metrics(
|
||
candidate_float, reference_fp32, fp32_denominator))
|
||
rounded_fp32_relative_l2, rounded_fp32_max_abs = (
|
||
_error_metrics(
|
||
reference_float, reference_fp32, fp32_denominator))
|
||
metrics = {
|
||
"relative_l2": relative_l2,
|
||
"max_abs": max_abs,
|
||
"candidate_to_fp32_relative_l2": (
|
||
candidate_fp32_relative_l2),
|
||
"candidate_to_fp32_max_abs": candidate_fp32_max_abs,
|
||
"rounded_to_fp32_relative_l2": rounded_fp32_relative_l2,
|
||
"rounded_to_fp32_max_abs": rounded_fp32_max_abs,
|
||
"relative_l2_baseline_ratio": (
|
||
candidate_fp32_relative_l2
|
||
/ max(
|
||
rounded_fp32_relative_l2,
|
||
_FUSED_PREFILL_SHADOW_RATIO_FLOOR,
|
||
)
|
||
),
|
||
"max_abs_baseline_ratio": (
|
||
candidate_fp32_max_abs
|
||
/ max(
|
||
rounded_fp32_max_abs,
|
||
_FUSED_PREFILL_SHADOW_RATIO_FLOOR,
|
||
)
|
||
),
|
||
}
|
||
return {
|
||
"status": (
|
||
"pass"
|
||
if _calibrated_shadow_metrics_qualified(metrics)
|
||
else "fail"
|
||
),
|
||
"candidate_finite": True,
|
||
"reference_finite": True,
|
||
**metrics,
|
||
}
|
||
qualified = (
|
||
relative_l2 <= _FUSED_PREFILL_SHADOW_RELATIVE_L2_LIMIT
|
||
and max_abs <= _FUSED_PREFILL_SHADOW_MAX_ABS_LIMIT)
|
||
return {
|
||
"status": "pass" if qualified else "fail",
|
||
"candidate_finite": True,
|
||
"reference_finite": True,
|
||
"relative_l2": relative_l2,
|
||
"max_abs": max_abs,
|
||
}
|
||
|
||
|
||
def _validate_decode_layout(
|
||
num_seqs: int,
|
||
seq_lens_count: int,
|
||
block_table_rows: int,
|
||
block_table_width: int,
|
||
actual_max: int,
|
||
block_size: int,
|
||
physical_key_blocks: int,
|
||
physical_value_blocks: int,
|
||
num_heads: int,
|
||
num_kv_heads: int,
|
||
) -> int:
|
||
"""Validate host-visible decode metadata before a native kernel launch."""
|
||
if num_seqs <= 0:
|
||
raise RuntimeError(f"decode requires num_seqs > 0, got {num_seqs}")
|
||
if seq_lens_count != num_seqs:
|
||
raise RuntimeError(
|
||
f"seq_lens has {seq_lens_count} entries for {num_seqs} sequences")
|
||
if block_table_rows < num_seqs:
|
||
raise RuntimeError(
|
||
f"block table has {block_table_rows} rows for {num_seqs} sequences")
|
||
if actual_max <= 0:
|
||
raise RuntimeError(f"decode sequence length must be > 0, got {actual_max}")
|
||
if block_size <= 0:
|
||
raise RuntimeError(f"KV block_size must be > 0, got {block_size}")
|
||
if physical_key_blocks != physical_value_blocks:
|
||
raise RuntimeError(
|
||
"key/value cache block counts differ: "
|
||
f"{physical_key_blocks} != {physical_value_blocks}")
|
||
if num_kv_heads <= 0 or num_heads % num_kv_heads != 0:
|
||
raise RuntimeError(
|
||
f"invalid GQA layout: num_heads={num_heads}, "
|
||
f"num_kv_heads={num_kv_heads}")
|
||
|
||
required_blocks = (actual_max + block_size - 1) // block_size
|
||
if required_blocks > block_table_width:
|
||
raise RuntimeError(
|
||
f"decode needs {required_blocks} blocks for seq_len={actual_max}, "
|
||
f"but block table width is {block_table_width}")
|
||
return required_blocks
|
||
|
||
|
||
def _strict_prefix_query_segments(
|
||
context_len: int,
|
||
query_len: int,
|
||
block_size: int,
|
||
) -> List[Tuple[int, int, int]]:
|
||
"""Split a query at the strict prefix-cache checkpoint, if it crosses it."""
|
||
if query_len <= 0:
|
||
return []
|
||
total_len = context_len + query_len
|
||
strict_prefix_len = ((total_len - 1) // block_size) * block_size
|
||
split = strict_prefix_len - context_len
|
||
if 0 < split < query_len:
|
||
return [(0, split, context_len),
|
||
(split, query_len, strict_prefix_len)]
|
||
return [(0, query_len, context_len)]
|
||
|
||
|
||
def _is_supported_corex_fused_paged_prefill_request(
|
||
kv_cache_dtype: str,
|
||
max_query_len: int,
|
||
total_query_len: int,
|
||
alibi_slopes: Optional[torch.Tensor],
|
||
sliding_window: Optional[int],
|
||
k_scale: float,
|
||
v_scale: float,
|
||
is_causal_decoder: bool,
|
||
) -> bool:
|
||
"""Check request-wide properties that are outside the native ABI."""
|
||
return bool(
|
||
is_causal_decoder
|
||
and _PREFIX_BLOCKS_PER_TILE == 32
|
||
and kv_cache_dtype == "auto"
|
||
and max_query_len == total_query_len
|
||
and alibi_slopes is None
|
||
and sliding_window is None
|
||
and k_scale == 1.0
|
||
and v_scale == 1.0
|
||
)
|
||
|
||
|
||
def _can_enable_corex_fused_paged_prefill_request(
|
||
kv_cache_dtype: str,
|
||
max_query_len: int,
|
||
total_query_len: int,
|
||
alibi_slopes: Optional[torch.Tensor],
|
||
sliding_window: Optional[int],
|
||
k_scale: float,
|
||
v_scale: float,
|
||
is_causal_decoder: bool,
|
||
) -> bool:
|
||
return bool(
|
||
_USE_COREX_FUSED_PAGED_PREFILL
|
||
and _is_supported_corex_fused_paged_prefill_request(
|
||
kv_cache_dtype,
|
||
max_query_len,
|
||
total_query_len,
|
||
alibi_slopes,
|
||
sliding_window,
|
||
k_scale,
|
||
v_scale,
|
||
is_causal_decoder,
|
||
)
|
||
)
|
||
|
||
|
||
def _is_single_sequence_fused_prefill_metadata(
|
||
batch_size: int,
|
||
block_table_rows: int,
|
||
query_start_count: int,
|
||
query_start_first: int,
|
||
query_start_last: int,
|
||
seq_lens_count: int,
|
||
seq_len: int,
|
||
context_lens_count: int,
|
||
context_len: int,
|
||
total_query_len: int,
|
||
) -> bool:
|
||
"""Validate the exact single-sequence metadata used by qualification."""
|
||
return bool(
|
||
batch_size == 1
|
||
and block_table_rows == 1
|
||
and query_start_count == 2
|
||
and query_start_first == 0
|
||
and query_start_last == total_query_len
|
||
and seq_lens_count == 1
|
||
and context_lens_count == 1
|
||
and context_len >= 0
|
||
and seq_len == context_len + total_query_len
|
||
and seq_len <= 262144
|
||
)
|
||
|
||
|
||
def _is_supported_corex_fused_paged_prefill_segment(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
prefix_key: torch.Tensor,
|
||
prefix_value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
seq_index: int,
|
||
block_context_len: int,
|
||
num_q_heads: int,
|
||
num_kv_heads: int,
|
||
head_dim: int,
|
||
gqa_ratio: int,
|
||
block_size: int,
|
||
) -> bool:
|
||
"""Accept only the fixed M1-47 production shape."""
|
||
query_len = query.shape[0]
|
||
if (
|
||
query_len <= 16
|
||
or query_len > 8192
|
||
or block_context_len < 0
|
||
or block_context_len % 16 != 0
|
||
or block_context_len + query_len > 262144
|
||
):
|
||
return False
|
||
if (num_q_heads, num_kv_heads, head_dim, gqa_ratio, block_size) != (
|
||
4,
|
||
1,
|
||
256,
|
||
4,
|
||
16,
|
||
):
|
||
return False
|
||
if prefix_key.shape[0] != 0 or prefix_value.shape[0] != 0:
|
||
return False
|
||
if (
|
||
tuple(query.shape) != (query_len, 4, 256)
|
||
or tuple(key.shape) != (query_len, 1, 256)
|
||
or tuple(value.shape) != (query_len, 1, 256)
|
||
):
|
||
return False
|
||
if (
|
||
len(key_cache.shape) != 5
|
||
or tuple(key_cache.shape[1:]) != (1, 32, 16, 8)
|
||
or len(value_cache.shape) != 4
|
||
or tuple(value_cache.shape[1:]) != (1, 256, 16)
|
||
or key_cache.shape[0] != value_cache.shape[0]
|
||
):
|
||
return False
|
||
if (
|
||
len(block_tables.shape) != 2
|
||
or block_tables.shape[0] != 1
|
||
or seq_index < 0
|
||
or seq_index >= block_tables.shape[0]
|
||
or block_tables.shape[1] < block_context_len // block_size
|
||
):
|
||
return False
|
||
|
||
half_tensors = (query, key, value, key_cache, value_cache)
|
||
if any(tensor.dtype != torch.float16 for tensor in half_tensors):
|
||
return False
|
||
if block_tables.dtype != torch.int32:
|
||
return False
|
||
tensors = half_tensors + (block_tables,)
|
||
if any(not tensor.is_cuda for tensor in tensors):
|
||
return False
|
||
if any(tensor.device != query.device for tensor in tensors):
|
||
return False
|
||
if any(not tensor.is_contiguous() for tensor in tensors):
|
||
return False
|
||
return True
|
||
|
||
|
||
def _can_use_corex_fused_paged_prefill(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
prefix_key: torch.Tensor,
|
||
prefix_value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
seq_index: int,
|
||
block_context_len: int,
|
||
num_q_heads: int,
|
||
num_kv_heads: int,
|
||
head_dim: int,
|
||
gqa_ratio: int,
|
||
block_size: int,
|
||
) -> bool:
|
||
return bool(
|
||
_USE_COREX_FUSED_PAGED_PREFILL
|
||
and _is_supported_corex_fused_paged_prefill_segment(
|
||
query,
|
||
key,
|
||
value,
|
||
prefix_key,
|
||
prefix_value,
|
||
key_cache,
|
||
value_cache,
|
||
block_tables,
|
||
seq_index,
|
||
block_context_len,
|
||
num_q_heads,
|
||
num_kv_heads,
|
||
head_dim,
|
||
gqa_ratio,
|
||
block_size,
|
||
)
|
||
)
|
||
|
||
|
||
def _prefix_context_tile_spans(
|
||
block_context_len: int,
|
||
prefix_query_len: int,
|
||
tile_size: int,
|
||
) -> List[Tuple[int, int, int, int]]:
|
||
"""Map context tiles to block-cache and preceding-query token ranges.
|
||
|
||
Each tuple is ``(block_start, block_end, prefix_start, prefix_end)``.
|
||
Concatenating both ranges reconstructs one tile in the logical context.
|
||
Keeping tiles aligned to absolute token positions makes cold segmented
|
||
prefill use the same online-softmax partitions as a warm cached request.
|
||
"""
|
||
if block_context_len < 0 or prefix_query_len < 0 or tile_size <= 0:
|
||
raise ValueError("context lengths must be non-negative and tile_size > 0")
|
||
spans = []
|
||
total_context_len = block_context_len + prefix_query_len
|
||
for tile_start in range(0, total_context_len, tile_size):
|
||
tile_end = min(tile_start + tile_size, total_context_len)
|
||
block_start = min(tile_start, block_context_len)
|
||
block_end = min(tile_end, block_context_len)
|
||
prefix_start = max(0, tile_start - block_context_len)
|
||
prefix_end = max(0, tile_end - block_context_len)
|
||
spans.append((block_start, block_end, prefix_start, prefix_end))
|
||
return spans
|
||
|
||
|
||
@dataclass
|
||
class PagedAttentionMetadata:
|
||
"""Metadata for PagedAttention."""
|
||
# (batch_size,). The length of sequences (entire tokens seen so far) per
|
||
# sequence.
|
||
seq_lens_tensor: Optional[torch.Tensor]
|
||
# Maximum sequence length in the batch. 0 if it is prefill-only batch.
|
||
max_decode_seq_len: int
|
||
# (batch_size, max_blocks_per_seq).
|
||
# Block addresses per sequence. (Seq id -> list of physical block)
|
||
# E.g., [0, 1, 2] means tokens are stored in 0th, 1st, and 2nd blocks
|
||
# in the kv cache. Each block can contain up to block_size tokens.
|
||
# 2nd dimensions are padded up to max_blocks_per_seq if it is cuda-graph
|
||
# captured.
|
||
block_tables: Optional[torch.Tensor]
|
||
|
||
|
||
class PagedAttention:
|
||
|
||
@staticmethod
|
||
def get_supported_head_sizes() -> List[int]:
|
||
return [64, 80, 96, 112, 120, 128, 192, 256]
|
||
|
||
@staticmethod
|
||
def get_kv_cache_shape(
|
||
num_blocks: int,
|
||
block_size: int,
|
||
num_kv_heads: int,
|
||
head_size: int,
|
||
) -> Tuple[int, ...]:
|
||
return (2, num_blocks, block_size * num_kv_heads * head_size)
|
||
|
||
@staticmethod
|
||
def split_kv_cache(
|
||
kv_cache: torch.Tensor,
|
||
num_kv_heads: int,
|
||
head_size: int,
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
x = 16 // kv_cache.element_size()
|
||
num_blocks = kv_cache.shape[1]
|
||
|
||
key_cache = kv_cache[0]
|
||
key_cache = key_cache.view(num_blocks, num_kv_heads, head_size // x,
|
||
-1, x)
|
||
value_cache = kv_cache[1]
|
||
value_cache = value_cache.view(num_blocks, num_kv_heads, head_size, -1)
|
||
return key_cache, value_cache
|
||
|
||
@staticmethod
|
||
def write_to_paged_cache(
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
slot_mapping: torch.Tensor,
|
||
kv_cache_dtype: str,
|
||
k_scale: float,
|
||
v_scale: float,
|
||
) -> None:
|
||
global _CACHE_WRITE_LOGGED
|
||
flat_slots = slot_mapping.flatten()
|
||
if key.shape[0] != value.shape[0]:
|
||
raise RuntimeError(
|
||
f"key/value token counts differ: {key.shape[0]} != "
|
||
f"{value.shape[0]}")
|
||
if flat_slots.numel() != key.shape[0]:
|
||
raise RuntimeError(
|
||
f"slot_mapping has {flat_slots.numel()} entries for "
|
||
f"{key.shape[0]} KV tokens")
|
||
if key_cache.shape[0] != value_cache.shape[0]:
|
||
raise RuntimeError(
|
||
"key/value cache block counts differ before cache write: "
|
||
f"{key_cache.shape[0]} != {value_cache.shape[0]}")
|
||
|
||
if _PAGED_ATTN_DIAGNOSTICS and flat_slots.numel() > 0:
|
||
min_slot = int(flat_slots.min().item())
|
||
max_slot = int(flat_slots.max().item())
|
||
max_valid_slot = key_cache.shape[0] * value_cache.shape[3] - 1
|
||
if min_slot < -1 or max_slot > max_valid_slot:
|
||
raise RuntimeError(
|
||
f"slot_mapping range [{min_slot}, {max_slot}] outside "
|
||
f"[-1, {max_valid_slot}]")
|
||
|
||
if _PAGED_ATTN_DIAGNOSTICS and not _CACHE_WRITE_LOGGED:
|
||
print(
|
||
"[BI100 PAGED_ATTN] cache_write "
|
||
f"pid={os.getpid()} rank={os.environ.get('RANK', '?')} "
|
||
f"local_rank={os.environ.get('LOCAL_RANK', '?')} "
|
||
f"key={tuple(key.shape)} value={tuple(value.shape)} "
|
||
f"slots={tuple(flat_slots.shape)} "
|
||
f"key_cache={tuple(key_cache.shape)} "
|
||
f"value_cache={tuple(value_cache.shape)}",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
_CACHE_WRITE_LOGGED = True
|
||
|
||
ops.reshape_and_cache(
|
||
key,
|
||
value,
|
||
key_cache,
|
||
value_cache,
|
||
flat_slots,
|
||
kv_cache_dtype,
|
||
k_scale,
|
||
v_scale,
|
||
)
|
||
if _PAGED_ATTN_DIAGNOSTICS:
|
||
try:
|
||
torch.cuda.synchronize()
|
||
except Exception as exc:
|
||
print(
|
||
"[BI100 PAGED_ATTN] cache_write_sync_failed "
|
||
f"pid={os.getpid()} error={type(exc).__name__}: {exc}",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
raise
|
||
|
||
@staticmethod
|
||
def _forward_decode_pytorch(
|
||
query: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
seq_lens: torch.Tensor,
|
||
scale: float,
|
||
) -> torch.Tensor:
|
||
"""Pure-PyTorch decode attention for long contexts (no hardware kernel).
|
||
|
||
paged_attention_v1 hangs on BI-V100 when max_seq_len > ~32K due to
|
||
shared memory limits. For decode, q_len=1 per sequence so no Q-tiling
|
||
is needed — the attention weight tensor is [H, 1, seq_len] which is
|
||
trivially small (~5 MB at 50K).
|
||
|
||
Shapes
|
||
------
|
||
query : [num_seqs, num_heads, head_dim]
|
||
key_cache : [num_blocks, num_kv_heads, head_dim//x, block_size, x]
|
||
value_cache : [num_blocks, num_kv_heads, head_dim, block_size]
|
||
block_tables: [num_seqs, max_blocks_per_seq]
|
||
seq_lens : [num_seqs]
|
||
"""
|
||
num_seqs, num_heads, head_dim = query.shape
|
||
num_kv_heads = key_cache.shape[1]
|
||
block_size = value_cache.shape[3]
|
||
gqa_ratio = num_heads // num_kv_heads
|
||
orig_dtype = query.dtype
|
||
|
||
output = torch.empty_like(query)
|
||
|
||
try:
|
||
for i in range(num_seqs):
|
||
seq_len = int(seq_lens[i].item())
|
||
num_blocks = (seq_len + block_size - 1) // block_size
|
||
blk_ids = block_tables[i, :num_blocks]
|
||
|
||
use_corex_gather = (
|
||
_USE_COREX_PAGED_KV_GATHER
|
||
and query.dtype == torch.float16
|
||
and key_cache.dtype == torch.float16
|
||
and value_cache.dtype == torch.float16
|
||
and block_tables.dtype == torch.int32
|
||
and key_cache.is_contiguous()
|
||
and value_cache.is_contiguous()
|
||
and blk_ids.is_contiguous())
|
||
if use_corex_gather:
|
||
k_t, v_t = _corex_paged_kv_gather.gather(
|
||
key_cache, value_cache, blk_ids, seq_len)
|
||
else:
|
||
# Gather K: [kv_h, head_dim, seq_len] fp32 without GQA
|
||
# expansion. The CoreX path above fuses these layout copies
|
||
# and FP16-to-FP32 conversions into one kernel.
|
||
k_t = (key_cache[blk_ids]
|
||
.permute(0, 3, 1, 2, 4)
|
||
.contiguous()
|
||
.view(-1, num_kv_heads, head_dim))[:seq_len] \
|
||
.permute(1, 2, 0).contiguous().float()
|
||
v_t = (value_cache[blk_ids]
|
||
.permute(0, 3, 1, 2)
|
||
.contiguous()
|
||
.view(-1, num_kv_heads, head_dim))[:seq_len] \
|
||
.permute(1, 0, 2).contiguous().float()
|
||
|
||
# Reshape Q for lazy GQA: [kv_h, gqa_ratio, 1, d]
|
||
q_grouped = (query[i].float()
|
||
.view(num_kv_heads, gqa_ratio, head_dim)
|
||
.unsqueeze(2))
|
||
|
||
# [kv_h, gqa_ratio, 1, seq_len]
|
||
attn_w = torch.matmul(
|
||
q_grouped * scale, # [kv_h, gqa, 1, d]
|
||
k_t.unsqueeze(1)) # [kv_h, 1, d, seq_len]
|
||
attn_w = torch.softmax(attn_w, dim=-1)
|
||
|
||
# [kv_h, gqa_ratio, 1, d] → [num_heads, head_dim]
|
||
out_i = torch.matmul(attn_w, v_t.unsqueeze(1))
|
||
output[i] = out_i.view(num_heads, head_dim).to(orig_dtype)
|
||
|
||
except Exception as e:
|
||
print(f"[decode_pytorch ERROR] {type(e).__name__}: {e}",
|
||
file=sys.stderr, flush=True)
|
||
traceback.print_exc(file=sys.stderr)
|
||
raise
|
||
|
||
return output
|
||
|
||
# paged_attention_v1 on BI-V100 fails for long contexts.
|
||
# Route on actual sequence length (seq_lens.max()), not the max_seq_len
|
||
# parameter which is inflated to max_model_len in CUDA graph mode.
|
||
_PYTORCH_DECODE_THRESHOLD = _PYTORCH_DECODE_THRESHOLD
|
||
_FORCE_PAGED_ATTN_V2 = _FORCE_PAGED_ATTN_V2
|
||
|
||
@staticmethod
|
||
def _should_use_paged_attention_v1(
|
||
max_seq_len: int,
|
||
max_num_partitions: int,
|
||
num_seqs: int,
|
||
num_heads: int,
|
||
) -> bool:
|
||
if PagedAttention._FORCE_PAGED_ATTN_V2:
|
||
return False
|
||
# Keep the stable BI100 default: V1 is used unless long-context decode
|
||
# has already routed to the PyTorch fallback above.
|
||
return True
|
||
|
||
@staticmethod
|
||
def _validate_prefix_block_table(
|
||
seq_index: int,
|
||
num_ctx_blocks: int,
|
||
block_table_width: int,
|
||
ctx_len: int,
|
||
) -> int:
|
||
if num_ctx_blocks <= block_table_width:
|
||
return num_ctx_blocks
|
||
msg = (
|
||
f"seq {seq_index}: num_ctx_blocks={num_ctx_blocks} "
|
||
f"> block_tables.shape[1]={block_table_width}, "
|
||
f"ctx_len={ctx_len}. Block table is undersized; "
|
||
"refusing to truncate context because attention would be incorrect.")
|
||
if env_bool("BI100_ALLOW_PREFIX_GUARD_CAP", False):
|
||
print(
|
||
"[paged_attn RISK] BI100_ALLOW_PREFIX_GUARD_CAP=1; "
|
||
f"{msg} Debug cap is enabled and may corrupt output.",
|
||
file=sys.stderr,
|
||
flush=True)
|
||
return block_table_width
|
||
raise RuntimeError(msg)
|
||
|
||
@staticmethod
|
||
def forward_decode(
|
||
query: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
seq_lens: torch.Tensor,
|
||
max_seq_len: int,
|
||
kv_cache_dtype: str,
|
||
head_mapping: torch.Tensor,
|
||
scale: float,
|
||
alibi_slopes: Optional[torch.Tensor],
|
||
k_scale: float,
|
||
v_scale: float,
|
||
tp_rank: int = 0,
|
||
blocksparse_local_blocks: int = 0,
|
||
blocksparse_vert_stride: int = 0,
|
||
blocksparse_block_size: int = 64,
|
||
blocksparse_head_sliding_step: int = 0,
|
||
) -> torch.Tensor:
|
||
actual_max = int(seq_lens.max().item()) if seq_lens.numel() > 0 else max_seq_len
|
||
block_size = value_cache.shape[3]
|
||
num_seqs, num_heads, head_size = query.shape
|
||
if key_cache.shape[1] != value_cache.shape[1]:
|
||
raise RuntimeError(
|
||
"key/value cache KV-head counts differ: "
|
||
f"{key_cache.shape[1]} != {value_cache.shape[1]}")
|
||
if head_mapping.numel() != num_heads:
|
||
raise RuntimeError(
|
||
f"head_mapping has {head_mapping.numel()} entries for "
|
||
f"{num_heads} query heads")
|
||
required_blocks = _validate_decode_layout(
|
||
num_seqs=num_seqs,
|
||
seq_lens_count=seq_lens.numel(),
|
||
block_table_rows=block_tables.shape[0],
|
||
block_table_width=block_tables.shape[1],
|
||
actual_max=actual_max,
|
||
block_size=block_size,
|
||
physical_key_blocks=key_cache.shape[0],
|
||
physical_value_blocks=value_cache.shape[0],
|
||
num_heads=num_heads,
|
||
num_kv_heads=key_cache.shape[1],
|
||
)
|
||
if actual_max > max_seq_len:
|
||
raise RuntimeError(
|
||
f"actual decode length {actual_max} exceeds max_seq_len "
|
||
f"{max_seq_len}")
|
||
|
||
if actual_max > PagedAttention._PYTORCH_DECODE_THRESHOLD:
|
||
path = ("pytorch_corex_gather" if _USE_COREX_PAGED_KV_GATHER
|
||
else "pytorch")
|
||
else:
|
||
path = "native_v1"
|
||
log_key = (path, None)
|
||
if (_DECODE_LOG_INTERVAL > 0 and
|
||
actual_max % _DECODE_LOG_INTERVAL == 0):
|
||
log_key = (path, actual_max)
|
||
if log_key not in _DECODE_DISPATCH_LOGGED:
|
||
print(
|
||
"[BI100 PAGED_ATTN] decode_dispatch "
|
||
f"pid={os.getpid()} rank={os.environ.get('RANK', '?')} "
|
||
f"local_rank={os.environ.get('LOCAL_RANK', '?')} "
|
||
f"path={path} actual_max={actual_max} "
|
||
f"max_seq_len={max_seq_len} query={tuple(query.shape)} "
|
||
f"key_cache={tuple(key_cache.shape)} "
|
||
f"value_cache={tuple(value_cache.shape)} "
|
||
f"block_tables={tuple(block_tables.shape)} "
|
||
f"required_blocks={required_blocks} "
|
||
f"threshold={PagedAttention._PYTORCH_DECODE_THRESHOLD}",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
_DECODE_DISPATCH_LOGGED.add(log_key)
|
||
|
||
if _PAGED_ATTN_DIAGNOSTICS:
|
||
for seq_index in range(num_seqs):
|
||
seq_len = int(seq_lens[seq_index].item())
|
||
if seq_len <= 0:
|
||
raise RuntimeError(
|
||
f"seq {seq_index}: decode length must be > 0, got {seq_len}")
|
||
seq_blocks = (seq_len + block_size - 1) // block_size
|
||
block_ids = block_tables[seq_index, :seq_blocks]
|
||
min_block = int(block_ids.min().item())
|
||
max_block = int(block_ids.max().item())
|
||
if min_block < 0 or max_block >= key_cache.shape[0]:
|
||
raise RuntimeError(
|
||
f"seq {seq_index}: physical block range "
|
||
f"[{min_block}, {max_block}] outside "
|
||
f"[0, {key_cache.shape[0] - 1}]")
|
||
|
||
# BI-V100: paged_attention_v1 supports max_context_len<=32768.
|
||
# For longer contexts, use v2 with layout conversion (5D→4D).
|
||
# v1 key: [blocks, kv_h, head_dim//x, block_size, x]
|
||
# v2 key: [blocks, kv_h, block_size, head_dim]
|
||
if actual_max > 32768:
|
||
num_kv_heads = key_cache.shape[1]
|
||
key_cache_v2 = (key_cache
|
||
.permute(0, 1, 3, 2, 4)
|
||
.contiguous()
|
||
.view(key_cache.shape[0], num_kv_heads,
|
||
block_size, head_size))
|
||
value_cache_v2 = (value_cache
|
||
.permute(0, 1, 3, 2)
|
||
.contiguous())
|
||
output = torch.empty_like(query)
|
||
_partition = 512
|
||
max_num_partitions = ((max_seq_len + _partition - 1) //
|
||
_partition)
|
||
tmp_output = torch.empty(
|
||
size=(num_seqs, num_heads, max_num_partitions, head_size),
|
||
dtype=output.dtype, device=output.device)
|
||
exp_sums = torch.empty(
|
||
size=(num_seqs, num_heads, max_num_partitions),
|
||
dtype=torch.float32, device=output.device)
|
||
max_logits = torch.empty_like(exp_sums)
|
||
import ixformer.functions as _ixf_F
|
||
_ixf_F.vllm_single_query_cached_kv_attention_v2(
|
||
output, _partition, exp_sums, max_logits, tmp_output,
|
||
query, key_cache_v2, value_cache_v2, head_mapping, scale,
|
||
block_tables, seq_lens, block_size, max_seq_len)
|
||
return output
|
||
|
||
if blocksparse_vert_stride is not None and blocksparse_vert_stride > 1:
|
||
# use blocksparse paged attention
|
||
block_size = value_cache.size(-1)
|
||
assert (blocksparse_block_size > 0 and
|
||
blocksparse_block_size % block_size == 0), \
|
||
(f"{blocksparse_block_size=} needs to be a multiple of"
|
||
f"{block_size=} used in block_tables.")
|
||
|
||
output = torch.empty_like(query)
|
||
max_num_partitions = ((max_seq_len + _PARTITION_SIZE - 1) //
|
||
_PARTITION_SIZE)
|
||
# NOTE(woosuk): We use a simple heuristic to decide whether to use
|
||
# PagedAttention V1 or V2. If the number of partitions is 1, we use
|
||
# V1 to avoid the overhead of reduction. Also, if the number of
|
||
# sequences or heads is large, we use V1 since there is enough work
|
||
# to parallelize.
|
||
# TODO(woosuk): Tune this heuristic.
|
||
# For context len > 8192, use V2 kernel to avoid shared memory shortage.
|
||
use_v1 = PagedAttention._should_use_paged_attention_v1(
|
||
max_seq_len, max_num_partitions, num_seqs, num_heads)
|
||
if use_v1:
|
||
# Run PagedAttention V1.
|
||
ops.paged_attention_v1(
|
||
output,
|
||
query,
|
||
key_cache,
|
||
value_cache,
|
||
head_mapping,
|
||
scale,
|
||
block_tables,
|
||
seq_lens,
|
||
block_size,
|
||
max_seq_len,
|
||
alibi_slopes,
|
||
)
|
||
else:
|
||
# Run PagedAttention V2.
|
||
assert _PARTITION_SIZE % block_size == 0
|
||
tmp_output = torch.empty(
|
||
size=(num_seqs, num_heads, max_num_partitions, head_size),
|
||
dtype=output.dtype,
|
||
device=output.device,
|
||
)
|
||
exp_sums = torch.empty(
|
||
size=(num_seqs, num_heads, max_num_partitions),
|
||
dtype=torch.float32,
|
||
device=output.device,
|
||
)
|
||
max_logits = torch.empty_like(exp_sums)
|
||
ops.paged_attention_v2(
|
||
output,
|
||
exp_sums,
|
||
max_logits,
|
||
tmp_output,
|
||
query,
|
||
key_cache,
|
||
value_cache,
|
||
head_mapping,
|
||
scale,
|
||
block_tables,
|
||
seq_lens,
|
||
block_size,
|
||
max_seq_len,
|
||
alibi_slopes,
|
||
kv_cache_dtype,
|
||
k_scale,
|
||
v_scale,
|
||
tp_rank,
|
||
blocksparse_local_blocks,
|
||
blocksparse_vert_stride,
|
||
blocksparse_block_size,
|
||
blocksparse_head_sliding_step,
|
||
)
|
||
return output
|
||
|
||
@staticmethod
|
||
def forward_prefix(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
kv_cache_dtype: str,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
query_start_loc: torch.Tensor,
|
||
seq_lens_tensor: torch.Tensor,
|
||
context_lens: torch.Tensor,
|
||
max_query_len: int,
|
||
alibi_slopes: Optional[torch.Tensor],
|
||
sliding_window: Optional[int],
|
||
k_scale: float,
|
||
v_scale: float,
|
||
is_causal_decoder: bool = False,
|
||
) -> torch.Tensor:
|
||
# NOTE: The Triton context_attention_fwd kernel hangs on Iluvatar
|
||
# BI-V100 hardware (same class of issue as cudnnFlashAttnForward).
|
||
# Use a pure-PyTorch fallback that reads the paged KV cache directly.
|
||
supported_request = bool(
|
||
(_USE_COREX_FUSED_PAGED_PREFILL
|
||
or _ACTIVATION_CAPTURE_ENABLED)
|
||
and _is_supported_corex_fused_paged_prefill_request(
|
||
kv_cache_dtype,
|
||
max_query_len,
|
||
query.shape[0],
|
||
alibi_slopes,
|
||
sliding_window,
|
||
k_scale,
|
||
v_scale,
|
||
is_causal_decoder,
|
||
))
|
||
fused_request_eligible = bool(
|
||
_USE_COREX_FUSED_PAGED_PREFILL and supported_request)
|
||
capture_request_eligible = bool(
|
||
_ACTIVATION_CAPTURE_ENABLED and supported_request)
|
||
_log_corex_fused_prefill_diagnostic(
|
||
"request",
|
||
eligible=fused_request_eligible,
|
||
use_native=_USE_COREX_FUSED_PAGED_PREFILL,
|
||
causal=is_causal_decoder,
|
||
kv_cache_dtype=kv_cache_dtype,
|
||
max_query_len=max_query_len,
|
||
total_query_len=query.shape[0],
|
||
tile_blocks=_PREFIX_BLOCKS_PER_TILE,
|
||
alibi_none=alibi_slopes is None,
|
||
sliding_window=sliding_window,
|
||
k_scale=k_scale,
|
||
v_scale=v_scale,
|
||
)
|
||
return PagedAttention._forward_prefix_pytorch(
|
||
query, key, value,
|
||
key_cache, value_cache,
|
||
block_tables, query_start_loc,
|
||
seq_lens_tensor, context_lens,
|
||
fused_request_eligible=fused_request_eligible,
|
||
capture_request_eligible=capture_request_eligible,
|
||
)
|
||
|
||
@staticmethod
|
||
def _forward_prefix_pytorch(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
query_start_loc: torch.Tensor,
|
||
seq_lens_tensor: torch.Tensor,
|
||
context_lens: torch.Tensor,
|
||
fused_request_eligible: bool = False,
|
||
capture_request_eligible: bool = False,
|
||
) -> torch.Tensor:
|
||
"""Pure-PyTorch prefix-attention with K-tiling (Flash-Attention online softmax).
|
||
|
||
Memory complexity: O(q_len), independent of kv_len.
|
||
Query segments end at the same strict block boundary used by prefix
|
||
caching. This keeps online-softmax reduction partitions identical when
|
||
an otherwise equivalent request reuses that prefix.
|
||
|
||
Algorithm: Flash Attention online softmax.
|
||
Q is reshaped once to [kv_h, gqa, q_len, d] (24 MB) and held for all
|
||
K-tiles. For each tile a running (m, l, o) accumulator is updated —
|
||
the [q_len × kv_len] attention matrix is NEVER materialised in full.
|
||
|
||
Tile budget (kv_h=1, gqa=6, q_len=4096, tile=256 tokens):
|
||
q_seq [1, 6, 4096, 256] fp32 24 MB (held all tiles)
|
||
o_acc same shape 24 MB (held all tiles)
|
||
s same shape 24 MB (per tile, freed before exp_s)
|
||
exp_s same shape 24 MB (per tile, brief overlap with s)
|
||
Peak ≈ 96 MB (s and exp_s briefly coexist during update).
|
||
|
||
Shapes
|
||
------
|
||
query : [total_q_tokens, num_q_heads, head_dim]
|
||
key : [total_q_tokens, num_kv_heads, head_dim]
|
||
value : [total_q_tokens, num_kv_heads, head_dim]
|
||
key_cache : [num_blocks, num_kv_heads, head_dim//x, block_size, x]
|
||
value_cache : [num_blocks, num_kv_heads, head_dim, block_size]
|
||
block_tables : [batch_size, max_blocks_per_seq]
|
||
query_start_loc: [batch_size + 1]
|
||
seq_lens_tensor: [batch_size] total length (context + query)
|
||
context_lens : [batch_size] tokens already in KV cache
|
||
"""
|
||
try:
|
||
profile_name = "paged_attn.prefix_pytorch"
|
||
# Paged-block tiles for context phase.
|
||
# tile_sz = _BLOCKS_PER_TILE × block_size (e.g. 16×16 = 256 tokens).
|
||
# Score tensor [kv_h, gqa, q_len, tile_sz] fp32 = 24 MB per tile.
|
||
# Same tile size reused for the current-chunk phase.
|
||
_BLOCKS_PER_TILE = _PREFIX_BLOCKS_PER_TILE
|
||
|
||
batch_size = seq_lens_tensor.shape[0]
|
||
num_q_heads = query.shape[1]
|
||
num_kv_heads = key_cache.shape[1]
|
||
head_dim = query.shape[2]
|
||
gqa_ratio = num_q_heads // num_kv_heads
|
||
block_size = value_cache.shape[3]
|
||
tile_sz = _BLOCKS_PER_TILE * block_size
|
||
scale = head_dim ** -0.5
|
||
orig_dtype = query.dtype
|
||
output = torch.empty_like(query)
|
||
|
||
if fused_request_eligible or capture_request_eligible:
|
||
query_start_count = query_start_loc.numel()
|
||
seq_lens_count = seq_lens_tensor.numel()
|
||
context_lens_count = context_lens.numel()
|
||
query_start_first = (
|
||
int(query_start_loc[0].item())
|
||
if query_start_count == 2 else -1)
|
||
query_start_last = (
|
||
int(query_start_loc[1].item())
|
||
if query_start_count == 2 else -1)
|
||
seq_len = (
|
||
int(seq_lens_tensor[0].item())
|
||
if seq_lens_count == 1 else -1)
|
||
context_len = (
|
||
int(context_lens[0].item())
|
||
if context_lens_count == 1 else -1)
|
||
metadata_eligible = (
|
||
_is_single_sequence_fused_prefill_metadata(
|
||
batch_size=batch_size,
|
||
block_table_rows=block_tables.shape[0],
|
||
query_start_count=query_start_count,
|
||
query_start_first=query_start_first,
|
||
query_start_last=query_start_last,
|
||
seq_lens_count=seq_lens_count,
|
||
seq_len=seq_len,
|
||
context_lens_count=context_lens_count,
|
||
context_len=context_len,
|
||
total_query_len=query.shape[0],
|
||
))
|
||
_log_corex_fused_prefill_diagnostic(
|
||
"metadata",
|
||
eligible=metadata_eligible,
|
||
batch_size=batch_size,
|
||
block_table_rows=block_tables.shape[0],
|
||
query_start_count=query_start_count,
|
||
query_start_first=query_start_first,
|
||
query_start_last=query_start_last,
|
||
seq_lens_count=seq_lens_count,
|
||
seq_len=seq_len,
|
||
context_lens_count=context_lens_count,
|
||
context_len=context_len,
|
||
total_query_len=query.shape[0],
|
||
)
|
||
fused_request_eligible = bool(
|
||
fused_request_eligible and metadata_eligible)
|
||
capture_request_eligible = bool(
|
||
capture_request_eligible and metadata_eligible)
|
||
|
||
for i in range(batch_size):
|
||
ctx_len = int(context_lens[i].item())
|
||
q_start = int(query_start_loc[i].item())
|
||
q_end = int(query_start_loc[i + 1].item())
|
||
q_len = q_end - q_start
|
||
|
||
for seg_start, seg_end, seg_ctx_len in (
|
||
_strict_prefix_query_segments(
|
||
ctx_len, q_len, block_size)):
|
||
absolute_start = q_start + seg_start
|
||
absolute_end = q_start + seg_end
|
||
bi100_profile_count(
|
||
"paged_attn.prefix_dispatch",
|
||
path="pytorch",
|
||
query_len=seg_end - seg_start,
|
||
request_query_len=q_len,
|
||
context_len=seg_ctx_len,
|
||
block_size=block_size,
|
||
query_heads=num_q_heads,
|
||
kv_heads=num_kv_heads,
|
||
head_dim=head_dim,
|
||
)
|
||
with bi100_timer(profile_name):
|
||
output[absolute_start:absolute_end] = (
|
||
PagedAttention._forward_prefix_segment_pytorch(
|
||
query[absolute_start:absolute_end],
|
||
key[absolute_start:absolute_end],
|
||
value[absolute_start:absolute_end],
|
||
key[q_start:absolute_start],
|
||
value[q_start:absolute_start],
|
||
key_cache,
|
||
value_cache,
|
||
block_tables,
|
||
i,
|
||
ctx_len,
|
||
num_q_heads,
|
||
num_kv_heads,
|
||
head_dim,
|
||
gqa_ratio,
|
||
block_size,
|
||
tile_sz,
|
||
scale,
|
||
orig_dtype,
|
||
fused_request_eligible=(
|
||
fused_request_eligible),
|
||
capture_request_eligible=(
|
||
capture_request_eligible),
|
||
))
|
||
|
||
except Exception as e:
|
||
print(f"[paged_attn ERROR] {type(e).__name__}: {e}",
|
||
file=sys.stderr, flush=True)
|
||
traceback.print_exc(file=sys.stderr)
|
||
raise
|
||
return output
|
||
|
||
@staticmethod
|
||
def _forward_prefix_segment_pytorch(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
prefix_key: torch.Tensor,
|
||
prefix_value: torch.Tensor,
|
||
key_cache: torch.Tensor,
|
||
value_cache: torch.Tensor,
|
||
block_tables: torch.Tensor,
|
||
seq_index: int,
|
||
block_context_len: int,
|
||
num_q_heads: int,
|
||
num_kv_heads: int,
|
||
head_dim: int,
|
||
gqa_ratio: int,
|
||
block_size: int,
|
||
tile_sz: int,
|
||
scale: float,
|
||
orig_dtype,
|
||
fused_request_eligible: bool = False,
|
||
return_fp32: bool = False,
|
||
capture_request_eligible: bool = False,
|
||
) -> torch.Tensor:
|
||
"""Run online-softmax attention for one strict-prefix query segment."""
|
||
q_len = query.shape[0]
|
||
supported_segment = (
|
||
_is_supported_corex_fused_paged_prefill_segment(
|
||
query,
|
||
key,
|
||
value,
|
||
prefix_key,
|
||
prefix_value,
|
||
key_cache,
|
||
value_cache,
|
||
block_tables,
|
||
seq_index,
|
||
block_context_len,
|
||
num_q_heads,
|
||
num_kv_heads,
|
||
head_dim,
|
||
gqa_ratio,
|
||
block_size,
|
||
))
|
||
segment_eligible = bool(
|
||
fused_request_eligible
|
||
and _USE_COREX_FUSED_PAGED_PREFILL
|
||
and supported_segment)
|
||
capture_segment_eligible = bool(
|
||
capture_request_eligible
|
||
and _ACTIVATION_CAPTURE_ENABLED
|
||
and supported_segment)
|
||
_log_corex_fused_prefill_diagnostic(
|
||
"segment",
|
||
eligible=segment_eligible,
|
||
request_eligible=fused_request_eligible,
|
||
query_shape=tuple(query.shape),
|
||
key_shape=tuple(key.shape),
|
||
value_shape=tuple(value.shape),
|
||
prefix_key_shape=tuple(prefix_key.shape),
|
||
prefix_value_shape=tuple(prefix_value.shape),
|
||
key_cache_shape=tuple(key_cache.shape),
|
||
value_cache_shape=tuple(value_cache.shape),
|
||
block_table_shape=tuple(block_tables.shape),
|
||
context_len=block_context_len,
|
||
seq_index=seq_index,
|
||
q_dtype=query.dtype,
|
||
block_table_dtype=block_tables.dtype,
|
||
query_cuda=query.is_cuda,
|
||
query_contiguous=query.is_contiguous(),
|
||
key_contiguous=key.is_contiguous(),
|
||
value_contiguous=value.is_contiguous(),
|
||
block_table_contiguous=block_tables.is_contiguous(),
|
||
heads=f"{num_q_heads}/{num_kv_heads}/{head_dim}",
|
||
gqa_ratio=gqa_ratio,
|
||
block_size=block_size,
|
||
)
|
||
if segment_eligible or capture_segment_eligible:
|
||
required_blocks = block_context_len // block_size
|
||
active_block_table = block_tables[
|
||
seq_index, :required_blocks].contiguous()
|
||
if capture_segment_eligible:
|
||
reservation = _reserve_activation_capture(block_context_len)
|
||
if reservation is not None:
|
||
_capture_fused_prefill_activation(
|
||
reservation,
|
||
query,
|
||
key,
|
||
value,
|
||
key_cache,
|
||
value_cache,
|
||
active_block_table,
|
||
block_context_len,
|
||
scale,
|
||
)
|
||
if segment_eligible:
|
||
shadow_index = _reserve_fused_prefill_shadow(
|
||
query,
|
||
block_context_len,
|
||
num_q_heads,
|
||
num_kv_heads,
|
||
head_dim,
|
||
block_size,
|
||
)
|
||
try:
|
||
fused_result = _corex_fused_paged_prefill.forward(
|
||
query,
|
||
key,
|
||
value,
|
||
key_cache,
|
||
value_cache,
|
||
active_block_table,
|
||
block_context_len,
|
||
scale,
|
||
)
|
||
except Exception as exc:
|
||
if shadow_index is not None:
|
||
_finish_fused_prefill_shadow(
|
||
shadow_index,
|
||
status="invalid",
|
||
error_stage="candidate-execution",
|
||
error_type=type(exc).__name__,
|
||
)
|
||
raise
|
||
if (
|
||
not isinstance(fused_result, (list, tuple))
|
||
or len(fused_result) != 2
|
||
):
|
||
if shadow_index is not None:
|
||
_finish_fused_prefill_shadow(
|
||
shadow_index,
|
||
status="invalid",
|
||
error_stage="candidate-contract",
|
||
error_type="InvalidResult",
|
||
)
|
||
raise RuntimeError(
|
||
"corex fused paged-prefill returned an invalid result")
|
||
fused_output = fused_result[0]
|
||
if (
|
||
tuple(fused_output.shape) != tuple(query.shape)
|
||
or fused_output.dtype != query.dtype
|
||
or fused_output.device != query.device
|
||
):
|
||
if shadow_index is not None:
|
||
_finish_fused_prefill_shadow(
|
||
shadow_index,
|
||
status="invalid",
|
||
error_stage="candidate-contract",
|
||
error_type="InvalidOutput",
|
||
)
|
||
raise RuntimeError(
|
||
"corex fused paged-prefill returned an invalid output")
|
||
if shadow_index is not None:
|
||
try:
|
||
reference_result = (
|
||
PagedAttention._forward_prefix_segment_pytorch(
|
||
query,
|
||
key,
|
||
value,
|
||
prefix_key,
|
||
prefix_value,
|
||
key_cache,
|
||
value_cache,
|
||
block_tables,
|
||
seq_index,
|
||
block_context_len,
|
||
num_q_heads,
|
||
num_kv_heads,
|
||
head_dim,
|
||
gqa_ratio,
|
||
block_size,
|
||
tile_sz,
|
||
scale,
|
||
orig_dtype,
|
||
fused_request_eligible=False,
|
||
capture_request_eligible=False,
|
||
return_fp32=(
|
||
_FUSED_PREFILL_SHADOW_NUMERIC_MODE
|
||
== "calibrated"),
|
||
))
|
||
reference_fp32 = (
|
||
reference_result
|
||
if _FUSED_PREFILL_SHADOW_NUMERIC_MODE
|
||
== "calibrated"
|
||
else None
|
||
)
|
||
reference_output = (
|
||
reference_result.to(orig_dtype)
|
||
if reference_fp32 is not None
|
||
else reference_result
|
||
)
|
||
shadow_metrics = _compare_fused_prefill_shadow_outputs(
|
||
fused_output,
|
||
reference_output,
|
||
reference_fp32,
|
||
)
|
||
except Exception as exc:
|
||
_finish_fused_prefill_shadow(
|
||
shadow_index,
|
||
status="invalid",
|
||
error_stage="reference-execution",
|
||
error_type=type(exc).__name__,
|
||
)
|
||
raise
|
||
_finish_fused_prefill_shadow(
|
||
shadow_index,
|
||
**shadow_metrics,
|
||
)
|
||
if (
|
||
shadow_metrics["status"] != "pass"
|
||
and (
|
||
_FUSED_PREFILL_SHADOW_FAILURE_ACTION == "raise"
|
||
or shadow_metrics["status"] == "invalid"
|
||
or shadow_metrics.get("candidate_finite") is not True
|
||
or shadow_metrics.get("reference_finite") is not True
|
||
)
|
||
):
|
||
raise RuntimeError(
|
||
"corex fused paged-prefill failed the real-activation "
|
||
"shadow-reference numerical gate")
|
||
log_key = "corex_split4"
|
||
if log_key not in _PREFIX_DISPATCH_LOGGED:
|
||
print(
|
||
"[BI100 PAGED_ATTN] prefix_dispatch "
|
||
f"pid={os.getpid()} rank={os.environ.get('RANK', '?')} "
|
||
f"local_rank={os.environ.get('LOCAL_RANK', '?')} "
|
||
f"path={log_key} context_len={block_context_len} "
|
||
f"query_len={q_len} required_blocks={required_blocks}",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
_PREFIX_DISPATCH_LOGGED.add(log_key)
|
||
return fused_output
|
||
|
||
dev = query.device
|
||
q_seq = (query.permute(1, 0, 2)
|
||
.float()
|
||
.view(num_kv_heads, gqa_ratio, q_len, head_dim)
|
||
.mul(scale))
|
||
m = torch.full((num_kv_heads, gqa_ratio, q_len),
|
||
float('-inf'), dtype=torch.float32, device=dev)
|
||
l = torch.zeros_like(m)
|
||
o = torch.zeros((num_kv_heads, gqa_ratio, q_len, head_dim),
|
||
dtype=torch.float32, device=dev)
|
||
|
||
if block_context_len > 0:
|
||
num_ctx_blocks = (block_context_len + block_size - 1) // block_size
|
||
num_ctx_blocks = PagedAttention._validate_prefix_block_table(
|
||
seq_index, num_ctx_blocks, block_tables.shape[1],
|
||
block_context_len)
|
||
|
||
for block_start, block_end, prefix_start, prefix_end in (
|
||
_prefix_context_tile_spans(
|
||
block_context_len, prefix_key.shape[0], tile_sz)):
|
||
k_parts = []
|
||
v_parts = []
|
||
if block_end > block_start:
|
||
first_block = block_start // block_size
|
||
last_block = (block_end + block_size - 1) // block_size
|
||
blk_ids = block_tables[seq_index, first_block:last_block]
|
||
k_blocks = (key_cache[blk_ids]
|
||
.permute(0, 3, 1, 2, 4)
|
||
.contiguous()
|
||
.view(-1, num_kv_heads, head_dim))
|
||
v_blocks = (value_cache[blk_ids]
|
||
.permute(0, 3, 1, 2)
|
||
.contiguous()
|
||
.view(-1, num_kv_heads, head_dim))
|
||
offset = block_start - first_block * block_size
|
||
length = block_end - block_start
|
||
k_parts.append(k_blocks[offset:offset + length])
|
||
v_parts.append(v_blocks[offset:offset + length])
|
||
if prefix_end > prefix_start:
|
||
k_parts.append(prefix_key[prefix_start:prefix_end])
|
||
v_parts.append(prefix_value[prefix_start:prefix_end])
|
||
k_context = (k_parts[0] if len(k_parts) == 1
|
||
else torch.cat(k_parts, dim=0))
|
||
v_context = (v_parts[0] if len(v_parts) == 1
|
||
else torch.cat(v_parts, dim=0))
|
||
k_t = (k_context.permute(1, 0, 2)
|
||
.unsqueeze(1).transpose(-1, -2).float())
|
||
v_t = v_context.permute(1, 0, 2).unsqueeze(1).float()
|
||
PagedAttention._update_online_softmax(q_seq, k_t, v_t, m, l, o)
|
||
|
||
for key_start in range(0, q_len, tile_sz):
|
||
key_end = min(key_start + tile_sz, q_len)
|
||
k_t = (key[key_start:key_end].permute(1, 0, 2)
|
||
.unsqueeze(1).transpose(-1, -2).float())
|
||
v_t = (value[key_start:key_end].permute(1, 0, 2)
|
||
.unsqueeze(1).float())
|
||
scores = torch.matmul(q_seq, k_t)
|
||
del k_t
|
||
key_positions = torch.arange(key_start, key_end, device=dev)
|
||
query_positions = torch.arange(q_len, device=dev)
|
||
mask = key_positions.unsqueeze(0) > query_positions.unsqueeze(1)
|
||
scores.masked_fill_(mask.unsqueeze(0).unsqueeze(0), float('-inf'))
|
||
del mask, key_positions, query_positions
|
||
PagedAttention._update_online_softmax_from_scores(
|
||
scores, v_t, m, l, o)
|
||
|
||
o.div_(l.unsqueeze(-1))
|
||
output = (
|
||
o.view(num_q_heads, q_len, head_dim)
|
||
.permute(1, 0, 2)
|
||
)
|
||
return output if return_fp32 else output.to(orig_dtype)
|
||
|
||
@staticmethod
|
||
def _update_online_softmax(
|
||
query: torch.Tensor,
|
||
key: torch.Tensor,
|
||
value: torch.Tensor,
|
||
running_max: torch.Tensor,
|
||
running_sum: torch.Tensor,
|
||
running_output: torch.Tensor,
|
||
) -> None:
|
||
scores = torch.matmul(query, key)
|
||
PagedAttention._update_online_softmax_from_scores(
|
||
scores, value, running_max, running_sum, running_output)
|
||
|
||
@staticmethod
|
||
def _update_online_softmax_from_scores(
|
||
scores: torch.Tensor,
|
||
value: torch.Tensor,
|
||
running_max: torch.Tensor,
|
||
running_sum: torch.Tensor,
|
||
running_output: torch.Tensor,
|
||
) -> None:
|
||
block_max = scores.amax(dim=-1)
|
||
new_max = torch.maximum(running_max, block_max)
|
||
exp_scores = scores - new_max.unsqueeze(-1)
|
||
del scores
|
||
exp_scores.exp_()
|
||
correction = torch.exp(running_max - new_max)
|
||
running_max.copy_(new_max)
|
||
running_sum.mul_(correction).add_(exp_scores.sum(dim=-1))
|
||
running_output.mul_(correction.unsqueeze(-1)).add_(
|
||
torch.matmul(exp_scores, value))
|
||
|
||
@staticmethod
|
||
def swap_blocks(
|
||
src_kv_cache: torch.Tensor,
|
||
dst_kv_cache: torch.Tensor,
|
||
src_to_dst: torch.Tensor,
|
||
) -> None:
|
||
src_key_cache = src_kv_cache[0]
|
||
dst_key_cache = dst_kv_cache[0]
|
||
ops.swap_blocks(src_key_cache, dst_key_cache, src_to_dst)
|
||
|
||
src_value_cache = src_kv_cache[1]
|
||
dst_value_cache = dst_kv_cache[1]
|
||
ops.swap_blocks(src_value_cache, dst_value_cache, src_to_dst)
|
||
|
||
@staticmethod
|
||
def copy_blocks(
|
||
kv_caches: List[torch.Tensor],
|
||
src_to_dists: torch.Tensor,
|
||
) -> None:
|
||
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||
import ixformer.functions as ixf_F
|
||
ixf_F.vllm_copy_cache(key_caches, value_caches, src_to_dists)
|