Files
project_6/qwen3_6_scripts/patch_xformers_sdpa_seq.py
Claude 336f3349ca fix(submit): restore flash_attn prefill + all 38eca5c2 improvements
Keeps ALL infrastructure from the last 70 commits:
- paged_attn.py: ixformer native v1/v2 decode dispatch (Output TPS impact)
- protocol.py: extra='allow' (fixes ~180 rejected replay requests)
- qwen3_5.py: .float() router_logits, chunk_recurrent, index_combine
- 14 prebuilt .so (including corex_gdn_chunk_recurrent)
- patch_xformers: flash_attn_varlen_func + profiling guard (>32K→Q-tiling)

yaml: max-num-seqs=2, TOPK=1, gpu-mem=0.90, max-model-len=131072
No LD_PRELOAD, no expandable_segments, no blocks cap hacks.
2026-08-14 03:47:09 +00:00

475 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
策略顺序per-sequencefallback — 纯 PyTorch 数学实现
==========================================================
逐条序列用 matmul + softmax 手写 attention完全绕开所有硬件
flash attention kernelixformer / cudnnFlashAttnForward
背景:
Iluvatar cudnnFlashAttnForward 存在两个已知问题:
1. 不支持 is_causal=True报错
2. 使用 attn_mask 路径时数值结果不正确(静默错误,输出全为"!"
与华为昇腾 910B4 上 llama.cpp --flash-attn off 修复同类问题的原理相同。
纯数学路径matmul + softmax在任何 PyTorch 后端上结果都正确。
优点:
数值正确,不依赖任何硬件特定 attention kernel。
峰值显存 = max(seq_len)² × H × dtype_size由 --max-model-len 控制。
缺点:
并发请求的 prefill attention 串行执行。
O(L²) 显存(无 flash attention 的 O(L) 优化)。
内存参考fp16H_local=6
max-model-len=4096 → 峰值 ~200 MB
max-model-len=8192 → 峰值 ~800 MB
max-model-len=16384 → 峰值 ~3.2 GB
额外 patcharg_utils.py
vllm 0.6.3 在 max_model_len > 32K 时会自动开启 chunked prefill无命令行
关闭选项),原意是防止 profiling OOM。但 _run_sdpa_fallback 已通过 Q-tiling
解决了该问题chunked prefill 反而会把推理路径从 _run_sdpa_fallback 切换到
_forward_prefix_pytorch属于不必要的行为变更因此一并禁用该自动逻辑。
Deploy:
python3 modified_scripts/patch_xformers_sdpa_seq.py
"""
from patch_utils import package_root, replace_one_of, replace_once
VLLM_ROOT = package_root("vllm")
XFORMERS_PATH = VLLM_ROOT / "attention" / "backends" / "xformers.py"
ARG_UTILS_PATH = VLLM_ROOT / "engine" / "arg_utils.py"
LOGITS_PROC_PATH = (
VLLM_ROOT / "model_executor" / "layers" / "logits_processor.py")
OUTLINES_DECODING_PATH = (
VLLM_ROOT / "model_executor" / "guided_decoding" /
"outlines_decoding.py")
# _apply_logits_processors crashes when seq_groups is None (intermediate
# chunked-prefill chunks on the driver rank). Add an early-return guard.
_LP_OLD_BLOCK = """\
def _apply_logits_processors(
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> torch.Tensor:
found_logits_processors = False\
"""
_LP_NEW_BLOCK = """\
def _apply_logits_processors(
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> torch.Tensor:
if sampling_metadata.seq_groups is None: # intermediate chunked-prefill chunk
return logits
found_logits_processors = False\
"""
# Outlines' UNESCAPED_STRING accepts raw JSON control characters, including
# newlines and tabs. The generated text can therefore satisfy the CFG while
# still failing json.loads(). Use the RFC 8259 string character constraints.
_JSON_STRING_OLD_BLOCK = """\
| UNESCAPED_STRING
| SIGNED_NUMBER -> number
| "true" -> true
| "false" -> false
| "null" -> null
array : "[" [value ("," value)*] "]"
object : "{" [pair ("," pair)*] "}"
pair : UNESCAPED_STRING ":" value
%import common.UNESCAPED_STRING
%import common.SIGNED_NUMBER
%import common.WS
%ignore WS\
"""
_JSON_STRING_V1_BLOCK = r'''| JSON_STRING
| SIGNED_NUMBER -> number
| "true" -> true
| "false" -> false
| "null" -> null
array : "[" [value ("," value)*] "]"
object : "{" [pair ("," pair)*] "}"
pair : JSON_STRING ":" value
JSON_STRING: /"(\\["\\\/bfnrt]|\\u[0-9a-fA-F]{4}|[^"\\\x00-\x1f])*"/
%import common.SIGNED_NUMBER
%import common.WS
%ignore WS'''
_JSON_STRING_NEW_BLOCK = r'''| JSON_STRING
| SIGNED_NUMBER -> number
| "true" -> true
| "false" -> false
| "null" -> null
array : "[" _ws [value (_ws "," _ws value)*] _ws "]"
object : "{" _ws [pair (_ws "," _ws pair)*] _ws "}"
pair : JSON_STRING _ws ":" _ws value
_ws : JSON_WS?
JSON_STRING: /"(\\["\\\/bfnrt]|\\u[0-9a-fA-F]{4}|[^"\\\x00-\x1f])*"/
JSON_WS: /[ \t\r\n]{1,4}/
%import common.SIGNED_NUMBER'''
# vllm 0.6.3 自动开启 chunked prefill 的原始块
_ARG_OLD_BLOCK = """\
if (is_gpu and not use_sliding_window and not use_spec_decode
and not self.enable_lora
and not self.enable_prompt_adapter):
self.enable_chunked_prefill = True
logger.warning(
"Chunked prefill is enabled by default for models with "
"max_model_len > 32K. Currently, chunked prefill might "
"not work with some features or models. If you "
"encounter any issues, please disable chunked prefill "
"by setting --enable-chunked-prefill=False.")\
"""
_ARG_NEW_BLOCK = """\
if (is_gpu and not use_sliding_window and not use_spec_decode
and not self.enable_lora
and not self.enable_prompt_adapter):
pass # skip auto-enable: Q-tiling in _run_sdpa_fallback
# handles long-context memory without chunked prefill\
"""
_MM_PREFIX_OLD_BLOCK = """\
if model_config.is_multimodal_model:
if self.enable_prefix_caching:
logger.warning(
"--enable-prefix-caching is currently not "
"supported for multimodal models and has been disabled.")
self.enable_prefix_caching = False\
"""
_MM_PREFIX_NEW_BLOCK = """\
if model_config.is_multimodal_model:
architectures = getattr(model_config.hf_config,
"architectures", []) or []
qwen36_native_vision = "Qwen3_5MoeForCausalLM" in architectures
if self.enable_prefix_caching and qwen36_native_vision:
logger.info(
"Keeping prefix caching enabled for the Qwen3.6 native "
"vision path.")
elif self.enable_prefix_caching:
logger.warning(
"--enable-prefix-caching is currently not "
"supported for multimodal models and has been disabled.")
self.enable_prefix_caching = False\
"""
FALLBACK_METHOD = '''
# --- flash_attn_varlen_func backend (loaded once) ---
# Import path: ixformer.contrib.vllm_flash_attn (canonical, matches
# ex_engine/python/corex_fa2.py Tier 1 and ixformer_sdk).
# Signature ref: ixformer_sdk/contrib/vllm_flash_attn/flash_attn_interface.py
_flash_varlen_func = None
_flash_varlen_checked = False
@classmethod
def _get_flash_varlen(cls):
if not cls._flash_varlen_checked:
cls._flash_varlen_checked = True
try:
from ixformer.contrib.vllm_flash_attn import (
flash_attn_varlen_func as _fn,
)
cls._flash_varlen_func = _fn
except ImportError:
pass
return cls._flash_varlen_func
def _run_sdpa_fallback(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: "XFormersMetadata",
) -> torch.Tensor:
"""Prefill attention fallback for head_dim > 128.
Dispatch priority (ref: ex_engine/python/corex_fa2.py):
1. ixformer flash_attn_varlen_func — fused kernel, O(L) memory
2. Pure-math Q-tiling fallback — safe for profiling / any HW
Profiling guard: when kv_cache is empty (profiling stage), vllm feeds
a dummy sequence up to max_model_len (131K). flash_attn temp buffers
at that length can exceed GPU memory. We use Q-tiling for profiling
(safe, correct, O(chunk × L) memory) and flash_attn for real
inference (fast, O(L) memory, verified on BI-V100 head_dim=256).
Args:
query : [1, total_query_tokens, num_heads, head_dim]
key : [1, total_query_tokens, num_kv_heads, head_dim]
value : [1, total_query_tokens, num_kv_heads, head_dim]
Returns:
[1, total_query_tokens, num_heads, head_dim]
"""
assert attn_metadata.seq_lens is not None
orig_dtype = query.dtype
num_seqs = len(attn_metadata.seq_lens)
max_seqlen = max(attn_metadata.seq_lens)
# Detect profiling: attn_metadata.num_prefill_tokens == total tokens
# AND no actual KV cache allocated yet (first forward pass).
# Also guard against very long dummy sequences (profiling uses
# max_model_len which can be 131K) where flash_attn would OOM.
_FLASH_SAFE_SEQLEN = 32768 # flash_attn temp buffers safe below this
is_profiling = (max_seqlen > _FLASH_SAFE_SEQLEN
and not hasattr(attn_metadata, '_has_real_kv_cache'))
# --- Path 1: flash_attn_varlen_func (real inference) ---
fn = self._get_flash_varlen()
if fn is not None and not is_profiling:
try:
q_flat = query.squeeze(0) # [T, H, D]
k_flat = key.squeeze(0) # [T, Hkv, D]
v_flat = value.squeeze(0)
cu_seqlens = torch.zeros(
num_seqs + 1, dtype=torch.int32, device=query.device)
for i, sl in enumerate(attn_metadata.seq_lens):
cu_seqlens[i + 1] = cu_seqlens[i] + sl
out = fn(
q=q_flat.to(torch.float16),
k=k_flat.to(torch.float16),
v=v_flat.to(torch.float16),
cu_seqlens_q=cu_seqlens,
cu_seqlens_k=cu_seqlens,
max_seqlen_q=max_seqlen,
max_seqlen_k=max_seqlen,
softmax_scale=self.scale,
causal=True,
)
return out.to(orig_dtype).unsqueeze(0)
except Exception:
pass # fall through to Q-tiling
# --- Path 2: Q-tiling (profiling or flash_attn unavailable) ---
_Q_CHUNK = 256
if (attn_metadata.query_start_loc is not None
and len(attn_metadata.query_start_loc) == num_seqs + 1):
q_lens = [
int(attn_metadata.query_start_loc[i + 1].item()) -
int(attn_metadata.query_start_loc[i].item())
for i in range(num_seqs)
]
else:
q_lens = list(attn_metadata.seq_lens)
q_flat = query.squeeze(0)
k_flat = key.squeeze(0)
v_flat = value.squeeze(0)
output = torch.empty_like(q_flat)
seq_start = 0
for q_len in q_lens:
seq_end = seq_start + q_len
k_s = k_flat[seq_start:seq_end].permute(1, 0, 2).float()
v_s = v_flat[seq_start:seq_end].permute(1, 0, 2).float()
if k_s.shape[0] != self.num_heads:
n = self.num_heads // k_s.shape[0]
k_s = k_s.repeat_interleave(n, dim=0).contiguous()
v_s = v_s.repeat_interleave(n, dim=0).contiguous()
k_pos = torch.arange(q_len, device=query.device)
for qc_start in range(0, q_len, _Q_CHUNK):
qc_end = min(qc_start + _Q_CHUNK, q_len)
q_c = q_flat[seq_start + qc_start:seq_start + qc_end] \
.permute(1, 0, 2).float()
attn_w = torch.matmul(q_c, k_s.transpose(-2, -1)) * self.scale
qc_q_pos = torch.arange(qc_start, qc_end, device=query.device)
mask = k_pos.unsqueeze(0) > qc_q_pos.unsqueeze(1)
attn_w = attn_w.masked_fill(mask.unsqueeze(0), float("-inf"))
attn_w = torch.softmax(attn_w, dim=-1)
out_c = torch.matmul(attn_w, v_s).to(orig_dtype)
output[seq_start + qc_start:seq_start + qc_end] = (
out_c.permute(1, 0, 2))
seq_start = seq_end
return output.unsqueeze(0)
'''
OLD_XFORMER_BLOCK = """\
self.attn_op = xops.fmha.flash.FwOp()
if self.alibi_slopes is None:
# Add the batch dimension.
query = query.unsqueeze(0)
key = key.unsqueeze(0)
value = value.unsqueeze(0)
out = xops.memory_efficient_attention_forward(
query,
key,
value,
attn_bias=attn_bias[0],
p=0.0,
scale=self.scale,
op = self.attn_op
)
return out.view_as(original_query)\
"""
NEW_XFORMER_BLOCK = """\
self.attn_op = xops.fmha.flash.FwOp()
if self.alibi_slopes is None:
# Add the batch dimension.
query = query.unsqueeze(0)
key = key.unsqueeze(0)
value = value.unsqueeze(0)
if self.head_size > 128:
out = self._run_sdpa_fallback(query, key, value, attn_metadata)
else:
out = xops.memory_efficient_attention_forward(
query,
key,
value,
attn_bias=attn_bias[0],
p=0.0,
scale=self.scale,
op=self.attn_op,
)
return out.view_as(original_query)\
"""
INJECT_ANCHOR = " def _run_memory_efficient_xformers_forward("
_PREFIX_CALL_OLD_BLOCK = """\
out = PagedAttention.forward_prefix(
query,
key,
value,
self.kv_cache_dtype,
key_cache,
value_cache,
prefill_meta.block_tables,
prefill_meta.query_start_loc,
prefill_meta.seq_lens_tensor,
prefill_meta.context_lens_tensor,
prefill_meta.max_query_len,
self.alibi_slopes,
self.sliding_window,
k_scale,
v_scale,
)\
"""
_PREFIX_CALL_NEW_BLOCK = """\
out = PagedAttention.forward_prefix(
query,
key,
value,
self.kv_cache_dtype,
key_cache,
value_cache,
prefill_meta.block_tables,
prefill_meta.query_start_loc,
prefill_meta.seq_lens_tensor,
prefill_meta.context_lens_tensor,
prefill_meta.max_query_len,
self.alibi_slopes,
self.sliding_window,
k_scale,
v_scale,
is_causal_decoder=(attn_type == AttentionType.DECODER),
)\
"""
def patch_file(path):
replace_once(
path,
INJECT_ANCHOR,
FALLBACK_METHOD + INJECT_ANCHOR,
required=True,
already_contains="def _run_sdpa_fallback(")
replace_once(
path,
OLD_XFORMER_BLOCK,
NEW_XFORMER_BLOCK,
required=True,
already_contains="out = self._run_sdpa_fallback(query, key, value, attn_metadata)")
replace_once(
path,
_PREFIX_CALL_OLD_BLOCK,
_PREFIX_CALL_NEW_BLOCK,
required=True,
already_contains=(
"is_causal_decoder=(attn_type == AttentionType.DECODER)"))
def patch_arg_utils(path):
replace_once(
path,
_ARG_OLD_BLOCK,
_ARG_NEW_BLOCK,
required=True,
already_contains="skip auto-enable: Q-tiling")
replace_once(
path,
_MM_PREFIX_OLD_BLOCK,
_MM_PREFIX_NEW_BLOCK,
required=True,
already_contains="Keeping prefix caching enabled for the Qwen3.6")
def patch_logits_processor(path):
replace_once(
path,
_LP_OLD_BLOCK,
_LP_NEW_BLOCK,
required=True,
already_contains="intermediate chunked-prefill chunk")
def patch_outlines_json_grammar(path):
replace_one_of(
path,
[
(_JSON_STRING_V1_BLOCK, _JSON_STRING_NEW_BLOCK),
(_JSON_STRING_OLD_BLOCK, _JSON_STRING_NEW_BLOCK),
],
required=True,
already_contains="JSON_WS:")
def main():
print("=== patch_xformers_sdpa_seq (sequential, pure-math) ===")
print(f"Target: {XFORMERS_PATH}")
patch_file(XFORMERS_PATH)
print("\n=== patch_arg_utils (disable chunked-prefill auto-enable) ===")
print(f"Target: {ARG_UTILS_PATH}")
patch_arg_utils(ARG_UTILS_PATH)
print("\n=== patch_logits_processor (seq_groups=None guard for chunked prefill) ===")
print(f"Target: {LOGITS_PROC_PATH}")
patch_logits_processor(LOGITS_PROC_PATH)
print("\n=== patch_outlines_json_grammar (reject raw control chars) ===")
print(f"Target: {OUTLINES_DECODING_PATH}")
patch_outlines_json_grammar(OUTLINES_DECODING_PATH)
print("\nDone.")
if __name__ == "__main__":
main()