XY-Serve 深度实战:当昇腾 NPU 终于学会「驯服动态负载」——从元原语分解、Token级调度到 Attention/Linear 双内核优化的完整工程指南(2026)
2026年3月,华为联合清华大学开源的 XY-Serve 论文被计算机体系结构顶会 ASPLOS'26 录用。这不是又一篇「刷榜」论文——它解决的是生产环境里一个真实存在的痛点:在 NPU 上高效运行不可预测的 LLM 动态负载。吞吐量提升95%、Attention 内核提速21.5%、Linear 内核提速14.6%……这些数字背后,是一套完整的「元原语」架构思维。
一、背景:为什么动态负载是 NPU 的噩梦
1.1 LLM 推理的两阶段特性
大语言模型推理分为两个截然不同的阶段:
预填充(Prefill)阶段:用户输入的完整 prompt(系统提示 + 历史对话 + 新消息)被一次性喂入模型。模型并行计算所有 token 的注意力,为每一层生成 Key 和 Value 矩阵。
这个阶段的瓶颈是纯算力(FLOPS)。GPU 算力跑满,batch 越大效率越高。
解码(Decode)阶段:一个 token 一个 token 地往外吐。每生成一个新 token,需要拿这个 token 的 Query 去和之前所有 token 的 Key 做注意力计算,然后加权 Value 得到输出。
这个阶段的瓶颈是内存带宽。从 HBM 读取 KV Cache 的速度决定了性能。
两个阶段的瓶颈完全不同——一个卡算力,一个卡带宽。这是理解整个 LLM 推理优化技术栈的钥匙。
1.2 动态性的本质挑战
生产环境中的 LLM 推理面临三种不可预测:
# 场景一:输入长度差异巨大
request_1 = "帮我翻译成中文:Hello" # 10 tokens
request_2 = "请详细分析这篇2万字的论文的核心论点..." # 20000+ tokens
# 场景二:输出长度无法预估
user: "写一首诗"
# 可能输出 20 tokens,也可能 500 tokens
# 场景三:混合工作负载
concurrent_requests = [
{"type": "chat", "input_len": 50, "expected_output": 100},
{"type": "code", "input_len": 5000, "expected_output": 2000},
{"type": "summary", "input_len": 10000, "expected_output": 500},
]
这种动态性对硬件调度提出极高要求。在 GPU 的 SIMT(单指令多线程)架构中,硬件可以自动重组或停用未使用的线程。但在 NPU 的基于块(Tile-based)架构中,处理不规则数据模式需要复杂的填充或掩码操作,带来额外开销。
1.3 NPU 架构的特殊约束
昇腾 NPU 采用达芬奇架构,核心计算单元是 AI Core,每个 AI Core 包含:
- Cube 单元:专门处理矩阵乘法(GEMM)
- Vector 单元:处理向量运算
- Scalar 单元:处理标量运算
与 GPU 的 SIMT 不同,NPU 采用基于块的执行模型:
GPU SIMT:一个 warp(32线程)执行相同指令,不同线程处理不同数据
- 自动处理分支分歧(部分线程执行,其他空闲)
- 硬件级线程调度
NPU Block:计算被划分为固定大小的块(如 16x16)
- 必须显式管理块的填充和掩码
- 不规则形状 → 额外填充开销
当输入长度不可预测时,矩阵形状任意变化:
Linear 层:[batch, seq_len, hidden] × [hidden, output]
- seq_len 动态变化
- 矩阵乘法形状不固定
Attention 层:[batch, heads, seq_len, head_dim]
- Q/K/V 长度不同(Prefill vs Decode)
- 掩码形状动态生成
这就是 XY-Serve 要解决的核心问题:如何在 NPU 上高效处理动态形状的计算,同时避免填充开销?
二、XY-Serve 架构全景:从动态分解到元内核
XY-Serve 的核心思想可以概括为一句话:将动态计算分解为硬件友好的元原语。
2.1 整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 请求入口层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP/gRPC │ │ OpenAI兼容 │ │ vLLM协议 │ │
│ │ API Gateway │ │ API Layer │ │ Adapter │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Token 级调度层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 自动前缀缓存(Auto Prefix Caching) │ │
│ │ - 匹配已有提示词前缀 │ │
│ │ - Token 级重用,避免重复计算 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 动态分解器(Dynamic Decomposer) │ │
│ │ - 将动态工作负载转换为固定大小的块 │ │
│ │ - 生成任务表(Task Table) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 任务重排层 │
│ ┌────────────────────┐ ┌────────────────────────────┐ │
│ │ Attention 任务重排 │ │ Linear 任务重排 │ │
│ │ - 按计算负载排序 │ │ - 优化 L2 缓存局部性 │ │
│ │ - 对称轮询分配 │ │ - 缓解 bank 冲突 │ │
│ └────────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 元内核层 │
│ ┌────────────────────┐ ┌────────────────────────────┐ │
│ │ Meta-Attention │ │ SmoothGEMM │ │
│ │ - GEMM-Softmax- │ │ - 固定块大小的矩阵乘法 │ │
│ │ GEMM 融合 │ │ - 虚拟填充技术 │ │
│ │ - 不区分P/D/V阶段 │ │ - 选择性 HBM 读写 │ │
│ └────────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 硬件抽象层(HAL) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Ascend NPU Driver + CANN Runtime │ │
│ │ - AI Core 调度 │ │
│ │ - 内存管理(HBM/Unified Buffer) │ │
│ │ - 事件同步 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 三个核心组件详解
组件一:Token 级调度与任务分解
传统调度以请求为单位,XY-Serve 以 Token 为单位:
class TokenScheduler:
def __init__(self, prefix_cache: PrefixCache):
self.prefix_cache = prefix_cache
self.task_queue = []
def process_request(self, request: Request):
# Step 1: 自动前缀缓存匹配
matched_tokens, unmatched_tokens = self.prefix_cache.match(
request.prompt_tokens
)
# Step 2: 只调度未匹配的 token
if unmatched_tokens:
self.task_queue.extend(
self.decomposer.decompose(unmatched_tokens)
)
# Step 3: 更新缓存
self.prefix_cache.update(request.prompt_tokens)
return matched_tokens, unmatched_tokens
动态分解机制的核心是将任意形状的计算划分为固定大小的块:
class DynamicDecomposer:
BLOCK_SIZE = 16 # NPU 友好的块大小
def decompose_attention(self, q_len, kv_len, num_heads):
"""
将 Attention 计算分解为块单元
原始计算:[batch, heads, q_len, head_dim] × [batch, heads, kv_len, head_dim]
分解后: 多个 [BLOCK_SIZE, BLOCK_SIZE] 的块
"""
tasks = []
for q_block in range(0, q_len, self.BLOCK_SIZE):
q_end = min(q_block + self.BLOCK_SIZE, q_len)
actual_q_size = q_end - q_block
for kv_block in range(0, kv_len, self.BLOCK_SIZE):
kv_end = min(kv_block + self.BLOCK_SIZE, kv_len)
actual_kv_size = kv_end - kv_block
# 记录实际大小,用于虚拟填充
tasks.append({
'q_range': (q_block, q_end),
'kv_range': (kv_block, kv_end),
'actual_shape': (actual_q_size, actual_kv_size),
'compute_load': actual_q_size * actual_kv_size
})
return tasks
组件二:任务重排机制
分解后的块需要智能调度,最大化硬件利用率。
Attention 任务重排:按计算负载从大到小排序,对称轮询分配给 AI Core。
class AttentionTaskReorder:
def __init__(self, num_ai_cores: int):
self.num_cores = num_ai_cores
def reorder(self, tasks: List[Task]):
"""
重排策略:
1. 计算每个块的计算负载(面积 = q_size × kv_size)
2. 从大到小排序
3. 对称轮询分配给各 AI Core
"""
# 按计算负载降序排序
sorted_tasks = sorted(
tasks,
key=lambda t: t['compute_load'],
reverse=True
)
# 对称轮询分配
core_queues = [[] for _ in range(self.num_cores)]
for i, task in enumerate(sorted_tasks):
core_idx = i % self.num_cores
core_queues[core_idx].append(task)
# 验证负载均衡
loads = [sum(t['compute_load'] for t in q) for q in core_queues]
max_load, min_load = max(loads), min(loads)
balance_ratio = min_load / max_load
assert balance_ratio > 0.95, f"负载不均衡: {balance_ratio}"
return core_queues
Linear 任务重排:优化 L2 缓存局部性和缓解 bank 冲突。
class LinearTaskReorder:
def reorder(self, tasks: List[Task], weight_layout: WeightLayout):
"""
重排策略:
1. 分析权重矩阵的 bank 分布
2. 按访问模式重排任务,最大化 bank 并行
3. 考虑 L2 缓存的命中率
"""
# 计算每个任务的 bank 访问模式
for task in tasks:
task['bank_pattern'] = self._analyze_bank_access(
task, weight_layout
)
# 按局部性分组
locality_groups = self._group_by_locality(tasks)
# 在组内按 bank 冲突最小化排序
for group in locality_groups:
group.sort(key=lambda t: t['bank_pattern'].conflict_score)
return locality_groups
def _analyze_bank_access(self, task, layout):
"""
分析任务对权重矩阵的访问模式
NPU 的 HBM 分为多个 bank,并行访问不同 bank 性能最优
"""
# 计算访问的地址范围
start_addr = layout.get_address(task['row_start'], task['col_start'])
end_addr = layout.get_address(task['row_end'], task['col_end'])
# 映射到 bank
banks_accessed = set()
for addr in range(start_addr, end_addr, layout.stride):
bank = (addr // layout.bank_size) % layout.num_banks
banks_accessed.add(bank)
return BankPattern(
banks=banks_accessed,
conflict_score=len(banks_accessed) / layout.num_banks
)
组件三:Meta-Attention 与 SmoothGEMM
这是 XY-Serve 的核心创新——元内核。
Meta-Attention:统一处理 GEMM-Softmax-GEMM 模式,不区分 Prefill/Decode/Verify 阶段。
// Meta-Attention 伪代码(C++ 内核实现)
__global__ void meta_attention_kernel(
// 输入
half* Q, // Query 矩阵
half* K, // Key 矩阵
half* V, // Value 矩阵
// 输出
half* output,
// 元数据
int* task_table, // 任务表
int num_tasks,
// 配置
int head_dim,
float scale
) {
// 1. 加载任务
__shared__ TaskBlock task_block;
if (threadIdx.x == 0) {
task_block = load_task(task_table, blockIdx.x);
}
__syncthreads();
// 2. 计算 Q × K^T(矩阵乘法)
__shared__ half Q_block[BLOCK_SIZE][BLOCK_SIZE];
__shared__ half K_block[BLOCK_SIZE][BLOCK_SIZE];
__shared__ half scores[BLOCK_SIZE][BLOCK_SIZE];
load_block(Q, Q_block, task_block.q_range);
load_block(K, K_block, task_block.kv_range);
// 使用 Cube 单元进行矩阵乘法
matmul_block(scores, Q_block, K_block, scale);
// 3. 应用掩码(虚拟填充,无实际开销)
if (task_block.actual_shape.q_size < BLOCK_SIZE ||
task_block.actual_shape.kv_size < BLOCK_SIZE) {
apply_virtual_mask(scores, task_block.actual_shape);
}
// 4. Softmax(Vector 单元)
__shared__ float attention_probs[BLOCK_SIZE][BLOCK_SIZE];
softmax_block(attention_probs, scores);
// 5. 计算 Attention × V(矩阵乘法)
__shared__ half V_block[BLOCK_SIZE][BLOCK_SIZE];
load_block(V, V_block, task_block.v_range);
__shared__ half out_block[BLOCK_SIZE][BLOCK_SIZE];
matmul_block(out_block, attention_probs, V_block);
// 6. 写回结果(选择性 HBM 读写)
store_block(output, out_block, task_block.out_range,
task_block.actual_shape);
}
SmoothGEMM:处理任意形状的矩阵乘法,无需实际填充。
// SmoothGEMM 核心思想:虚拟填充
__global__ void smooth_gemm_kernel(
half* A, half* B, half* C,
int M, int N, int K, // 实际矩阵形状
int block_M, int block_N, int block_K // 固定块大小
) {
// 计算当前块负责的区域
int m_start = blockIdx.y * block_M;
int n_start = blockIdx.x * block_N;
int m_end = min(m_start + block_M, M);
int n_end = min(n_start + block_N, N);
// 虚拟填充:只加载实际数据,不分配填充内存
__shared__ half A_tile[block_M][block_K];
__shared__ half B_tile[block_K][block_N];
// 加载时自动处理边界
for (int k = 0; k < K; k += block_K) {
// 加载 A 的块(可能不完整)
load_tile_virtual(A, A_tile,
m_start, m_end,
k, min(k + block_K, K));
// 加载 B 的块(可能不完整)
load_tile_virtual(B, B_tile,
k, min(k + block_K, K),
n_start, n_end);
// 计算(Cube 单元)
matmul_tile(C, A_tile, B_tile,
m_end - m_start,
n_end - n_start,
min(block_K, K - k));
}
// 写回时只写实际数据
store_tile_virtual(C, m_start, m_end, n_start, n_end);
}
// 虚拟加载:只读取有效数据
void load_tile_virtual(half* src, half dst[][block_K],
int row_start, int row_end,
int col_start, int col_end) {
int actual_rows = row_end - row_start;
int actual_cols = col_end - col_start;
// 只加载有效区域
for (int i = 0; i < actual_rows; i++) {
for (int j = 0; j < actual_cols; j++) {
dst[i][j] = src[(row_start + i) * stride + (col_start + j)];
}
}
// 不需要填充剩余部分——虚拟化处理
// 在计算时通过掩码跳过无效数据
}
2.3 关键优化技术详解
技术一:虚拟填充(Virtual Padding)
传统方法在处理不规则形状时需要物理填充:
# 传统方法:物理填充
def traditional_approach(matrix, target_shape):
"""需要实际分配填充内存"""
padded = np.zeros(target_shape, dtype=matrix.dtype)
padded[:matrix.shape[0], :matrix.shape[1]] = matrix
return padded # 浪费内存和带宽
XY-Serve 采用虚拟填充:
class VirtualPadding:
"""不分配填充内存,通过掩码实现"""
def __init__(self, actual_shape, block_shape):
self.actual = actual_shape
self.block = block_shape
def compute_with_mask(self, compute_fn, *args):
"""
在计算时动态生成掩码:
1. 只加载实际数据
2. 计算时掩码跳过无效位置
3. 只写回实际结果
"""
# 生成掩码(在片上内存中,极快)
mask = self._generate_mask()
# 执行计算
result = compute_fn(*args)
# 应用掩码
result = self._apply_mask(result, mask)
return result
def _generate_mask(self):
"""
掩码在片上内存(Unified Buffer)中生成
开销极小(几个周期的 Vector 单元操作)
"""
mask = np.ones(self.block, dtype=np.bool_)
mask[self.actual[0]:, :] = False
mask[:, self.actual[1]:] = False
return mask
性能对比:
场景:处理一个 [7, 13] 的矩阵(需要填充到 [16, 16])
传统物理填充:
- 内存分配:16 × 16 × 2 bytes = 512 bytes
- 填充开销:复制 7×13=91 个元素,填充 165 个零
- 总带宽:512 bytes 读 + 512 bytes 写 = 1024 bytes
XY-Serve 虚拟填充:
- 无额外内存分配
- 只加载 91 个元素
- 掩码开销:16 × 16 / 128 = 2 个 Vector 操作(约 100 纳秒)
- 总带宽:182 bytes(仅有效数据)
带宽节省:1024 / 182 = 5.6 倍
技术二:自动前缀缓存
Agent 场景中,System Prompt、工具定义、Few-shot 示例等前缀高度重复。XY-Serve 实现了 Token 级的前缀缓存:
class PrefixCache:
def __init__(self, cache_size: int):
self.cache = {} # prefix_hash -> (kv_cache, token_ids)
self.cache_size = cache_size
def match(self, prompt_tokens: List[int]) -> Tuple[int, int]:
"""
Token 级匹配:
1. 从最长前缀开始匹配
2. 返回匹配的 token 数量和未匹配的部分
"""
best_match_len = 0
best_match_key = None
# 尝试所有可能的前缀长度
for length in range(len(prompt_tokens), 0, -1):
prefix_hash = self._hash_tokens(prompt_tokens[:length])
if prefix_hash in self.cache:
best_match_len = length
best_match_key = prefix_hash
break
if best_match_len > 0:
kv_cache = self.cache[best_match_key]
return best_match_len, prompt_tokens[best_match_len:]
return 0, prompt_tokens
def update(self, tokens: List[int], kv_cache: KVCache):
"""更新缓存(LRU 策略)"""
prefix_hash = self._hash_tokens(tokens)
# LRU 淘汰
if len(self.cache) >= self.cache_size:
self._evict_lru()
self.cache[prefix_hash] = kv_cache
实战效果:
# Agent 场景示例
system_prompt = "你是一个专业的编程助手..." # 500 tokens
tools_def = "可用工具:\n1. execute_code\n2. read_file..." # 200 tokens
few_shot = "示例:\n问:...\n答:..." # 300 tokens
# 第一个请求:需要完整计算
request_1 = system_prompt + tools_def + few_shot + "帮我写个函数"
# Prefill 计算:1000 tokens
# 第二个请求:前缀完全匹配
request_2 = system_prompt + tools_def + few_shot + "帮我优化这段代码"
# Prefill 计算:只计算最后的 "帮我优化这段代码"(约 10 tokens)
# 节省:99% 的 Prefill 计算
三、性能评估:95% 吞吐提升从何而来
3.1 内核级性能对比
XY-Serve 在华为昇腾 910B 上进行了详细测试:
Attention 内核性能:
测试配置:Llama-3-70B,8 卡并行,batch_size=32
场景 Baseline XY-Serve 提升
────────────────────────────────────────────────
Coding (长输入) 1280 tok/s 1560 tok/s +21.9%
Conversation 1420 tok/s 1725 tok/s +21.5%
Mixed Workload 1350 tok/s 1650 tok/s +22.2%
平均提升:+21.5%
Linear 内核性能:
测试配置:动态形状矩阵乘法
矩阵形状 (M×N×K) Baseline XY-Serve 提升
────────────────────────────────────────────────
1024×1024×1024 1.00 ms 0.86 ms +14%
512×2048×1024 0.52 ms 0.45 ms +13.5%
2048×512×2048 1.05 ms 0.89 ms +15.2%
平均提升:+14.6%
3.2 端到端性能对比
公开数据集测试:
# 测试配置
model = "Llama-3-70B"
hardware = "Ascend 910B × 8"
dataset = "ShareGPT-V3"
# XY-Serve vs Ascend-vLLM
metrics = {
'throughput': {
'ascend_vllm': 1250, # tok/s
'xy_serve': 2362, # tok/s
'improvement': '+89%'
},
'ttft': { # Time To First Token
'ascend_vllm': 1.2, # s
'xy_serve': 0.43, # s
'improvement': '-64%'
},
'tbt': { # Time Between Tokens
'ascend_vllm': 45, # ms
'xy_serve': 19, # ms
'improvement': '-57%'
}
}
内部行业工作负载测试:
场景:企业级 RAG 系统,混合长文本问答
工作负载特征:
- 平均输入长度:3500 tokens
- 平均输出长度:800 tokens
- 前缀重复率:72%
结果:
- 吞吐量提升:+95%
- 平均延迟降低:-68%
- P99 延迟降低:-72%
3.3 动态调度优化效果
XY-Serve 的动态调度进一步提升了性能:
# 启用动态调度前后对比
dynamic_scheduling_results = {
'qps_improvement': '+89%', # QPS 提升
'tbt_latency_reduction': '-69%', # 平均 TBT 降低
'hardware_utilization': {
'before': '47%', # 硬件利用率
'after': '89%' # 硬件利用率
}
}
3.4 跨平台验证
XY-Serve 的核心技术也迁移到了 GPU 平台验证通用性:
GPU 平台(NVIDIA A100)测试结果:
Meta-Attention:
- Coding 场景延迟降低:-11%
- Conversation 场景延迟降低:-15%
SmoothGEMM:
- 硬件利用率提升:+18%
结论:元原语思想跨架构有效
四、代码实战:从零构建 XY-Serve 核心组件
4.1 环境准备
# 硬件要求:华为昇腾 910B 或以上
# 软件环境:
# - CANN 8.0+
# - Python 3.9+
# - PyTorch 2.1+
# 克隆代码
git clone https://gitee.com/openeuler/GVirt
cd GVirt/xlite
# 安装依赖
pip install -r requirements.txt
4.2 实现 Token 级调度器
import torch
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass
from collections import OrderedDict
@dataclass
class TokenBlock:
"""Token 块的定义"""
token_ids: List[int]
block_id: int
ref_count: int = 0 # 引用计数,用于缓存管理
class TokenScheduler:
"""XY-Serve 风格的 Token 级调度器"""
def __init__(
self,
block_size: int = 16,
max_cache_blocks: int = 10000,
num_ai_cores: int = 32
):
self.block_size = block_size
self.max_cache_blocks = max_cache_blocks
self.num_ai_cores = num_ai_cores
# 前缀缓存
self.prefix_cache: OrderedDict[int, TokenBlock] = OrderedDict()
# 任务队列
self.task_queue: List[Dict] = []
# KV Cache 管理
self.kv_allocator = KVCacheAllocator(max_cache_blocks)
def schedule_request(
self,
request_tokens: List[int]
) -> Tuple[List[TokenBlock], List[int]]:
"""
调度单个请求
返回:
- matched_blocks: 匹配的缓存块
- unmatched_tokens: 未匹配的 token
"""
# Step 1: 尝试前缀缓存匹配
matched_blocks, unmatched_tokens = self._match_prefix(request_tokens)
# Step 2: 为未匹配的 token 分配新块
if unmatched_tokens:
new_blocks = self._allocate_blocks(unmatched_tokens)
self.task_queue.extend(new_blocks)
return matched_blocks, unmatched_tokens
def _match_prefix(
self,
tokens: List[int]
) -> Tuple[List[TokenBlock], List[int]]:
"""前缀匹配算法"""
matched_blocks = []
# 逐块匹配
for i in range(0, len(tokens), self.block_size):
block_tokens = tokens[i:i+self.block_size]
block_hash = self._hash_block(block_tokens)
if block_hash in self.prefix_cache:
# 命中缓存
cached_block = self.prefix_cache[block_hash]
cached_block.ref_count += 1
matched_blocks.append(cached_block)
# 移动到末尾(LRU)
self.prefix_cache.move_to_end(block_hash)
else:
# 未命中,返回剩余 token
return matched_blocks, tokens[i:]
# 全部匹配
return matched_blocks, []
def _allocate_blocks(self, tokens: List[int]) -> List[Dict]:
"""为新 token 分配块并生成任务"""
blocks = []
for i in range(0, len(tokens), self.block_size):
block_tokens = tokens[i:i+self.block_size]
actual_size = len(block_tokens)
# 分配 KV Cache 空间
block_id = self.kv_allocator.allocate()
# 创建任务
task = {
'block_id': block_id,
'token_ids': block_tokens,
'actual_size': actual_size,
'needs_padding': actual_size < self.block_size,
'compute_load': actual_size * actual_size # 用于重排
}
blocks.append(task)
return blocks
def _hash_block(self, tokens: List[int]) -> int:
"""计算 token 块的哈希值"""
return hash(tuple(tokens))
def reorder_tasks_for_attention(
self,
tasks: List[Dict]
) -> List[List[Dict]]:
"""Attention 任务重排"""
# 按计算负载降序排序
sorted_tasks = sorted(
tasks,
key=lambda t: t['compute_load'],
reverse=True
)
# 对称轮询分配
core_queues = [[] for _ in range(self.num_ai_cores)]
for i, task in enumerate(sorted_tasks):
core_idx = i % self.num_ai_cores
core_queues[core_idx].append(task)
return core_queues
def get_schedule_stats(self) -> Dict:
"""获取调度统计信息"""
return {
'cache_hit_rate': self._calc_cache_hit_rate(),
'avg_compute_balance': self._calc_compute_balance(),
'pending_tasks': len(self.task_queue)
}
class KVCacheAllocator:
"""KV Cache 内存分配器"""
def __init__(self, max_blocks: int):
self.max_blocks = max_blocks
self.free_blocks = list(range(max_blocks))
self.used_blocks = set()
def allocate(self) -> int:
"""分配一个块"""
if not self.free_blocks:
raise RuntimeError("KV Cache 内存不足")
block_id = self.free_blocks.pop(0)
self.used_blocks.add(block_id)
return block_id
def free(self, block_id: int):
"""释放一个块"""
if block_id in self.used_blocks:
self.used_blocks.remove(block_id)
self.free_blocks.append(block_id)
def get_usage(self) -> float:
"""获取内存使用率"""
return len(self.used_blocks) / self.max_blocks
4.3 实现 Meta-Attention 内核
import torch
import torch.nn as nn
import torch.nn.functional as F
class MetaAttention(nn.Module):
"""
XY-Serve 风格的 Meta-Attention 内核
统一处理 GEMM-Softmax-GEMM 模式
不区分 Prefill/Decode/Verify 阶段
"""
def __init__(
self,
hidden_size: int,
num_heads: int,
head_dim: int,
block_size: int = 16,
max_seq_len: int = 4096
):
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = head_dim
self.block_size = block_size
self.max_seq_len = max_seq_len
# 投影矩阵
self.q_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
self.k_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
self.v_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
self.o_proj = nn.Linear(num_heads * head_dim, hidden_size, bias=False)
# 缩放因子
self.scale = 1.0 / (head_dim ** 0.5)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
task_table: Optional[List[Dict]] = None
) -> torch.Tensor:
"""
Meta-Attention 前向传播
Args:
hidden_states: [batch, seq_len, hidden_size]
attention_mask: [batch, 1, seq_len, total_seq_len]
kv_cache: (past_key, past_value)
task_table: 任务表(来自分解器)
Returns:
output: [batch, seq_len, hidden_size]
"""
batch_size, seq_len, _ = hidden_states.shape
# Step 1: 计算 Q、K、V
query = self.q_proj(hidden_states)
key = self.k_proj(hidden_states)
value = self.v_proj(hidden_states)
# Reshape
query = query.view(batch_size, seq_len, self.num_heads, self.head_dim)
key = key.view(batch_size, seq_len, self.num_heads, self.head_dim)
value = value.view(batch_size, seq_len, self.num_heads, self.head_dim)
# Transpose for attention
query = query.transpose(1, 2) # [b, h, s, d]
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# Step 2: 更新 KV Cache
if kv_cache is not None:
past_key, past_value = kv_cache
key = torch.cat([past_key, key], dim=2)
value = torch.cat([past_value, value], dim=2)
# Step 3: 分块计算(核心创新)
if task_table is not None:
output = self._block_attention_with_tasks(
query, key, value, task_table
)
else:
output = self._block_attention(query, key, value, attention_mask)
# Reshape back
output = output.transpose(1, 2).contiguous()
output = output.view(batch_size, seq_len, -1)
# Output projection
output = self.o_proj(output)
return output
def _block_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None
) -> torch.Tensor:
"""
分块 Attention 计算
实现 Meta-Attention 的核心:
1. 将计算划分为固定大小的块
2. 使用虚拟填充处理不规则形状
3. 融合 GEMM-Softmax-GEMM
"""
batch_size, num_heads, q_len, head_dim = query.shape
_, _, kv_len, _ = key.shape
output = torch.zeros_like(query)
# 分块计算
for q_start in range(0, q_len, self.block_size):
q_end = min(q_start + self.block_size, q_len)
q_block = query[:, :, q_start:q_end, :] # [b, h, block, d]
actual_q_size = q_end - q_start
# 累积 softmax 的分子和分母(用于跨块 softmax)
block_outputs = []
block_weights = []
for kv_start in range(0, kv_len, self.block_size):
kv_end = min(kv_start + self.block_size, kv_len)
k_block = key[:, :, kv_start:kv_end, :]
v_block = value[:, :, kv_start:kv_end, :]
actual_kv_size = kv_end - kv_start
# 计算 Q × K^T
scores = torch.matmul(q_block, k_block.transpose(-1, -2))
scores = scores * self.scale
# 虚拟掩码(只掩码实际大小的范围)
if actual_q_size < self.block_size or actual_kv_size < self.block_size:
mask = torch.ones(
actual_q_size, actual_kv_size,
device=scores.device, dtype=scores.dtype
)
# 对 padding 位置赋负无穷
padded_mask = F.pad(
mask,
(0, self.block_size - actual_kv_size,
0, self.block_size - actual_q_size),
value=float('-inf')
)
scores = scores + padded_mask.unsqueeze(0).unsqueeze(0)
# Softmax
attention_weights = F.softmax(scores, dim=-1)
# 只取有效部分
attention_weights = attention_weights[:, :, :actual_q_size, :actual_kv_size]
# 计算 Attention × V
block_output = torch.matmul(attention_weights, v_block[:, :, :actual_kv_size, :])
block_outputs.append(block_output)
block_weights.append(attention_weights.sum(dim=-1, keepdim=True))
# 合并块结果(简化版,实际需要更复杂的跨块 softmax)
output[:, :, q_start:q_end, :] = torch.cat(block_outputs, dim=2).mean(dim=2, keepdim=True).expand(-1, -1, actual_q_size, -1)
return output
def _block_attention_with_tasks(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
task_table: List[Dict]
) -> torch.Tensor:
"""
使用任务表的分块 Attention
这是 XY-Serve 的完整实现:
任务表已经包含了重排后的计算顺序
"""
# 实际在 NPU 上,这里会调用 C++ 内核
# Python 版本用于理解和调试
output = torch.zeros_like(query)
for task in task_table:
q_range = task['q_range']
kv_range = task['kv_range']
q_block = query[:, :, q_range[0]:q_range[1], :]
k_block = key[:, :, kv_range[0]:kv_range[1], :]
v_block = value[:, :, kv_range[0]:kv_range[1], :]
# GEMM-Softmax-GEMM 融合
scores = torch.matmul(q_block, k_block.transpose(-1, -2)) * self.scale
attention_weights = F.softmax(scores, dim=-1)
block_output = torch.matmul(attention_weights, v_block)
# 写回
output[:, :, q_range[0]:q_range[1], :] = block_output
return output
4.4 实现动态分解器
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class ComputeTask:
"""计算任务的定义"""
task_id: int
task_type: str # 'attention' or 'linear'
# 输入范围
input_ranges: Dict
# 实际形状(用于虚拟填充)
actual_shape: tuple
# 计算负载(用于重排)
compute_load: float
# 依赖关系
dependencies: List[int]
class DynamicDecomposer:
"""
XY-Serve 的动态分解器
将动态工作负载转换为硬件友好的块单元
"""
def __init__(
self,
block_size: int = 16,
max_seq_len: int = 8192
):
self.block_size = block_size
self.max_seq_len = max_seq_len
self.task_counter = 0
def decompose_attention(
self,
q_len: int,
kv_len: int,
num_heads: int,
batch_size: int
) -> List[ComputeTask]:
"""
分解 Attention 计算
返回任务列表,每个任务对应一个块单元
"""
tasks = []
for q_block in range(0, q_len, self.block_size):
q_end = min(q_block + self.block_size, q_len)
actual_q_size = q_end - q_block
for kv_block in range(0, kv_len, self.block_size):
kv_end = min(kv_block + self.block_size, kv_len)
actual_kv_size = kv_end - kv_block
# 创建任务
task = ComputeTask(
task_id=self.task_counter,
task_type='attention',
input_ranges={
'q_range': (q_block, q_end),
'k_range': (kv_block, kv_end),
'v_range': (kv_block, kv_end)
},
actual_shape=(actual_q_size, actual_kv_size),
compute_load=actual_q_size * actual_kv_size * num_heads,
dependencies=[]
)
tasks.append(task)
self.task_counter += 1
return tasks
def decompose_linear(
self,
M: int,
N: int,
K: int,
batch_size: int
) -> List[ComputeTask]:
"""
分解 Linear 计算(矩阵乘法)
"""
tasks = []
# 按输出维度分块
for m_block in range(0, M, self.block_size):
m_end = min(m_block + self.block_size, M)
actual_m_size = m_end - m_block
for n_block in range(0, N, self.block_size):
n_end = min(n_block + self.block_size, N)
actual_n_size = n_end - n_block
# K 维度可能需要多次累加
for k_block in range(0, K, self.block_size):
k_end = min(k_block + self.block_size, K)
actual_k_size = k_end - k_block
# 判断是否需要累加
if k_block > 0:
# 依赖前一个 k 块的任务
dep_task_id = self.task_counter - 1
else:
dep_task_id = None
task = ComputeTask(
task_id=self.task_counter,
task_type='linear',
input_ranges={
'a_range': (m_block, m_end, k_block, k_end),
'b_range': (k_block, k_end, n_block, n_end),
'c_range': (m_block, m_end, n_block, n_end)
},
actual_shape=(actual_m_size, actual_n_size, actual_k_size),
compute_load=actual_m_size * actual_n_size * actual_k_size,
dependencies=[dep_task_id] if dep_task_id else []
)
tasks.append(task)
self.task_counter += 1
return tasks
def analyze_load_balance(
self,
tasks: List[ComputeTask],
num_cores: int
) -> Dict:
"""
分析负载均衡情况
返回:
- 每个核心的总负载
- 负载均衡率(min/max)
- 建议的重排策略
"""
# 按负载排序
sorted_tasks = sorted(tasks, key=lambda t: t.compute_load, reverse=True)
# 模拟轮询分配
core_loads = [0.0] * num_cores
for i, task in enumerate(sorted_tasks):
core_idx = i % num_cores
core_loads[core_idx] += task.compute_load
# 计算均衡率
max_load = max(core_loads)
min_load = min(core_loads)
balance_ratio = min_load / max_load if max_load > 0 else 1.0
return {
'core_loads': core_loads,
'balance_ratio': balance_ratio,
'max_load': max_load,
'min_load': min_load,
'suggestion': 'good' if balance_ratio > 0.95 else 'needs_reorder'
}
4.5 完整的推理流程
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
class XYServeInference:
"""XY-Serve 完整推理流程"""
def __init__(
self,
model_path: str,
device: str = "cuda", # 或 "npu"
block_size: int = 16,
max_cache_size: int = 10000
):
self.device = device
self.block_size = block_size
# 加载模型
self.model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16
).to(device)
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
# 初始化组件
self.scheduler = TokenScheduler(
block_size=block_size,
max_cache_blocks=max_cache_size
)
self.decomposer = DynamicDecomposer(block_size=block_size)
# KV Cache
self.kv_cache = None
def generate(
self,
prompt: str,
max_new_tokens: int = 100,
temperature: float = 1.0,
top_p: float = 0.9
) -> str:
"""生成文本"""
# Tokenize
input_ids = self.tokenizer.encode(prompt, return_tensors="pt").to(self.device)
# Step 1: Token 级调度
prompt_tokens = input_ids[0].tolist()
matched_blocks, unmatched_tokens = self.scheduler.schedule_request(prompt_tokens)
print(f"缓存命中:{len(matched_blocks)} 块")
print(f"需要计算:{len(unmatched_tokens)} tokens")
# Step 2: 分解未匹配部分的计算
if unmatched_tokens:
tasks = self.decomposer.decompose_attention(
q_len=len(unmatched_tokens),
kv_len=len(prompt_tokens),
num_heads=self.model.config.num_attention_heads,
batch_size=1
)
# 分析负载均衡
analysis = self.decomposer.analyze_load_balance(tasks, num_cores=32)
print(f"负载均衡率:{analysis['balance_ratio']:.2%}")
# Step 3: Prefill
with torch.no_grad():
outputs = self.model(
input_ids,
past_key_values=self.kv_cache,
use_cache=True
)
self.kv_cache = outputs.past_key_values
# Step 4: Decode
generated_ids = []
current_token = outputs.logits[:, -1, :]
for _ in range(max_new_tokens):
# 采样下一个 token
next_token = self._sample(current_token, temperature, top_p)
generated_ids.append(next_token.item())
# Decode 步骤(只需要计算一个 token)
with torch.no_grad():
outputs = self.model(
next_token.unsqueeze(0),
past_key_values=self.kv_cache,
use_cache=True
)
self.kv_cache = outputs.past_key_values
current_token = outputs.logits[:, -1, :]
# 检查结束
if next_token.item() == self.tokenizer.eos_token_id:
break
# Detokenize
generated_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
return generated_text
def _sample(
self,
logits: torch.Tensor,
temperature: float,
top_p: float
) -> torch.Tensor:
"""采样"""
logits = logits / temperature
# Top-p 过滤
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
# 移除累积概率超过 top_p 的 token
sorted_indices_to_remove = cumulative_probs > top_p
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
sorted_indices_to_remove[..., 0] = 0
indices_to_remove = sorted_indices_to_remove.scatter(
1, sorted_indices, sorted_indices_to_remove
)
logits = logits.masked_fill(indices_to_remove, float('-inf'))
# 采样
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
return next_token
def get_stats(self) -> Dict:
"""获取运行统计"""
return {
'scheduler_stats': self.scheduler.get_schedule_stats(),
'kv_cache_usage': self.scheduler.kv_allocator.get_usage()
}
# 使用示例
if __name__ == "__main__":
inference = XYServeInference(
model_path="meta-llama/Llama-3-8B",
block_size=16
)
# 第一次请求
response = inference.generate(
"你是一个专业的编程助手。请帮我写一个快速排序算法。",
max_new_tokens=200
)
print(f"Response: {response}")
print(f"Stats: {inference.get_stats()}")
# 第二次请求(前缀会命中缓存)
response = inference.generate(
"你是一个专业的编程助手。请帮我写一个归并排序算法。",
max_new_tokens=200
)
print(f"Response: {response}")
print(f"Stats: {inference.get_stats()}") # 缓存命中率应该很高
五、与 vLLM/SGLang 的对比分析
5.1 架构对比
| 特性 | vLLM | SGLang | XY-Serve |
|---|---|---|---|
| 核心创新 | PagedAttention | RadixAttention | Meta-Attention |
| 目标硬件 | GPU (NVIDIA) | GPU (NVIDIA) | NPU (昇腾) |
| 动态形状处理 | 物理填充 + 填充开销 | 基数树优化 | 虚拟填充 + 无开销 |
| 前缀缓存 | Block 级 | Radix 树级 | Token 级 |
| 任务调度 | Continuous Batching | 连续批处理 + 约束解码 | Token 级 + 任务重排 |
| 硬件亲和度 | 高(GPU 原生) | 高(GPU 原生) | 极高(NPU 原生) |
5.2 性能对比
# 测试配置
benchmark_config = {
'model': 'Llama-3-70B',
'hardware_npu': 'Ascend 910B × 8',
'hardware_gpu': 'NVIDIA A100 × 8',
'dataset': 'ShareGPT-V3',
'batch_size': 32
}
# 吞吐量对比 (tok/s)
throughput_results = {
'vLLM (A100)': 1850,
'SGLang (A100)': 2100,
'XY-Serve (910B)': 2362,
}
# 延迟对比 (ms)
latency_results = {
'vLLM (A100)': {'ttft': 0.8, 'tbt': 25},
'SGLang (A100)': {'ttft': 0.6, 'tbt': 22},
'XY-Serve (910B)': {'ttft': 0.43, 'tbt': 19},
}
5.3 适用场景
vLLM 最适合:
- NVIDIA GPU 环境
- 通用 LLM 推理
- 需要丰富生态支持
SGLang 最适合:
- 需要结构化输出
- Agent 场景(约束解码)
- 长 KV Cache 复用
XY-Serve 最适合:
- 华为昇腾 NPU 环境
- 动态输入输出长度
- 生产级高并发场景
- 前缀高度重复的 Agent 应用
六、生产部署最佳实践
6.1 容器化部署
# Dockerfile for XY-Serve
FROM openeuler/openeuler:22.03-lts
# 安装 CANN
RUN yum install -y \
Ascend-cann-toolkit \
Ascend-cann-kernels \
Ascend-cann-nnae
# 安装 Python 依赖
RUN pip install \
torch==2.1.0 \
transformers==4.36.0 \
fastapi==0.109.0 \
uvicorn==0.27.0
# 复制 XY-Serve 代码
COPY xlite /opt/xlite
WORKDIR /opt/xlite
# 暴露端口
EXPOSE 8000
# 启动服务
CMD ["python", "server.py", "--port", "8000", "--num-workers", "8"]
# server.py - OpenAI 兼容的 API 服务
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
app = FastAPI(title="XY-Serve API")
# 初始化推理引擎
inference_engine = XYServeInference(
model_path="/models/llama-3-70b",
device="npu",
block_size=16
)
class ChatCompletionRequest(BaseModel):
model: str
messages: List[dict]
max_tokens: Optional[int] = 100
temperature: Optional[float] = 1.0
top_p: Optional[float] = 0.9
stream: Optional[bool] = False
class ChatCompletionResponse(BaseModel):
id: str
object: str = "chat.completion"
choices: List[dict]
usage: dict
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
"""OpenAI 兼容的聊天接口"""
# 构造 prompt
prompt = "\n".join([
f"{m['role']}: {m['content']}"
for m in request.messages
])
# 生成
response = inference_engine.generate(
prompt,
max_new_tokens=request.max_tokens,
temperature=request.temperature,
top_p=request.top_p
)
return ChatCompletionResponse(
id=f"chatcmpl-{uuid.uuid4().hex[:8]}",
choices=[{
"index": 0,
"message": {
"role": "assistant",
"content": response
},
"finish_reason": "stop"
}],
usage={
"prompt_tokens": len(inference_engine.tokenizer.encode(prompt)),
"completion_tokens": len(inference_engine.tokenizer.encode(response)),
"total_tokens": len(inference_engine.tokenizer.encode(prompt + response))
}
)
@app.get("/health")
async def health():
"""健康检查"""
stats = inference_engine.get_stats()
return {
"status": "healthy",
"kv_cache_usage": stats['kv_cache_usage'],
"cache_hit_rate": stats['scheduler_stats']['cache_hit_rate']
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
6.2 Kubernetes 部署
# xyserve-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: xyserve-inference
namespace: ai-inference
spec:
replicas: 3
selector:
matchLabels:
app: xyserve
template:
metadata:
labels:
app: xyserve
spec:
containers:
- name: xyserve
image: xyserve:latest
ports:
- containerPort: 8000
resources:
limits:
ascend.ai/vnpu: "Ascend910B" # NPU 资源
env:
- name: MODEL_PATH
value: "/models/llama-3-70b"
- name: BLOCK_SIZE
value: "16"
- name: MAX_CACHE_SIZE
value: "50000"
volumeMounts:
- name: model-storage
mountPath: /models
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-pvc
---
apiVersion: v1
kind: Service
metadata:
name: xyserve-service
namespace: ai-inference
spec:
selector:
app: xyserve
ports:
- port: 80
targetPort: 8000
type: LoadBalancer
6.3 监控与调优
# 监控指标
from prometheus_client import Counter, Histogram, Gauge
# 定义指标
request_counter = Counter(
'xyserve_requests_total',
'Total number of inference requests'
)
latency_histogram = Histogram(
'xyserve_latency_seconds',
'Inference latency in seconds',
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)
cache_hit_rate = Gauge(
'xyserve_cache_hit_rate',
'Prefix cache hit rate'
)
kv_cache_usage = Gauge(
'xyserve_kv_cache_usage',
'KV Cache memory usage'
)
# 在推理流程中记录指标
def monitored_generate(prompt: str, **kwargs):
request_counter.inc()
with latency_histogram.time():
result = inference_engine.generate(prompt, **kwargs)
# 更新缓存指标
stats = inference_engine.get_stats()
cache_hit_rate.set(stats['scheduler_stats']['cache_hit_rate'])
kv_cache_usage.set(stats['kv_cache_usage'])
return result
七、总结与展望
7.1 XY-Serve 的核心贡献
XY-Serve 解决了一个真实存在的生产痛点:在 NPU 上高效运行动态 LLM 负载。它的三大核心创新:
- 元原语分解:将动态计算转换为硬件友好的固定大小块
- 虚拟填充:消除物理填充开销,节省带宽和内存
- Token 级调度:实现细粒度的前缀缓存和负载均衡
7.2 对行业的启示
XY-Serve 的成功说明:
- NPU 不是 GPU 的简单替代:需要针对架构特性设计专门的优化策略
- 动态性是 LLM 推理的核心挑战:解决它需要系统级的创新
- 开源与合作的力量:华为 + 清华的产学研结合,ASPLOS 论文的开源
7.3 未来发展方向
- 更细粒度的任务调度:从 Token 级到操作符级
- 跨模型优化:支持 MoE、多模态等新架构
- 异构协同:NPU + GPU 混合部署
- 自动化调优:基于 RL 的自动参数优化
XY-Serve 的代码已在 Gitee 开源:https://gitee.com/openeuler/GVirt/tree/master/xlite
论文地址:https://doi.org/10.1145/3760250.3762228
这不是又一篇「刷榜」论文——它是生产级推理系统的工程实践,是中国 AI 基础设施的一次重要突破。