基于 AMD Ryzen AI,通过 KV 缓存复用加速本地大语言模型对话
Jun 11, 2026
本地大语言模型正在催生全新的 AI PC 应用,包括私人文档助手、编码辅助工具、对话式智能体以及特定领域的协作助手。在本地运行推理可减少对网络连接的依赖,并且当应用配置为本地执行时,有助于将提示和应用数据保留在设备上。随着这些应用的对话属性不断增强,在多回合对话过程中维持低延迟变得愈发关键。在常规的聊天会话中,每条新用户消息都会被添加到现有的对话历史记录中。如果缺乏高效的上下文保留机制,模型在生成回复前就必须重复处理整个对话。
这种重复处理发生在预填充阶段,在此阶段,模型会将输入 token 转换为生成回复所需的内部注意力状态。随着对话长度增加,预填充阶段可能会成为影响整体响应延迟的重要因素。
KV 缓存复用通过在对话回合之间保留模型的内部注意力状态,来应对这一挑战。借助 KV 缓存复用,应用无需针对每次请求都重新构建整个对话上下文,而仅需处理新增 token,同时复用之前已处理的信息。
AMD Ryzen AI 软件 1.7.1 依托 ONNX Runtime GenAI 的连续解码 API,支持 KV 缓存复用。在本博客中,我们将探索 KV 缓存复用的工作原理,演示多回合对话处理及对话回退功能,并衡量因避免重复预填充计算所带来的低延迟优势。
大语言模型分两个阶段处理文本:
预填充 — 处理输入的提示并构建注意力状态。
解码 — 一次生成一个输出 token。
对于许多对话类工作负载,预填充阶段是主要算力开销来源。
以一次简单的对话为例:
第一回合:[系统提示] + [用户消息 1]
第二回合:[系统提示] + [用户消息 1] + [回复 1] + [用户消息 2]
第三回合:[系统提示] + [用户消息 1] + [回复 1] + [用户消息 2] + [回复 2] + ...
如果缺乏上下文复用机制,模型在每个回合都需要重新处理整个对话。即使大部分内容已处理过,模型仍需从头开始重新构建其注意力状态。
随着对话历史记录的长度增加,响应延迟会增大,消耗的计算资源也会增多。
借助 KV 缓存复用,先前处理过的注意力信息会在各回合之间一直保留。如此一来,模型仅需处理新增 token,从而大幅减少重复处理工作。
不使用缓存 |
使用缓存 |
在每个回合都需进行完整的预填充 |
仅针对新 token 执行预填充 |
延迟随对话长度增加而增长 |
每个回合的延迟近乎恒定 |
长对话功耗更高 |
每次响应的能耗更低 |
理解 KV 缓存
基于 Transformer 的大语言模型采用自注意力机制,将输入中的每个 token 与所有其他 token 相关联。对于每个注意力层,模型会为每个 token 计算三个矩阵:查询 (Q)、键 (K) 和值 (V)。
键和值矩阵是开销较高的部分:模型必须先为上下文中的每个 token 计算这两个矩阵,才能生成任何输出。键值 (KV) 缓存本质就是将相应计算结果保存下来。
如果不使用 KV 缓存复用:
第 N 回合提示 = [Token 1 ...Token N]
模型在每一回合都要为全部 N 个 token 重新计算 K 和 V 值
如果使用 KV 缓存复用:
第一回合后: KV 缓存 = [K1, V1]
第二回合后: KV 缓存 = [K1, V1, K2, V2] <- 仅新增 K2、V2
第三回合后: KV 缓存 = [K1, V1, K2, V2, K3, V3] <- 仅新增 K3、V3
KV 缓存复用意味着,模型只需为增量部分(即自上一回合以来新增的 token)执行注意力计算,同时读取此前处理过的所有内容的缓存状态。
ONNX Runtime GenAI 中的连续解码
Ryzen AI 软件 1.7.1 通过 ONNX Runtime GenAI 的连续解码功能,对外提供 KV 缓存复用功能。
从应用角度来看,这可实现:
- 持续的多回合对话
- 增量 token 追加
- 上下文保留,而无需重建提示
- 对话回退与分支工作流程
该功能直接在运行时中实现,仅需安装标准 Ryzen AI 软件,而无需额外安装任何库。
硬件与软件支持
所有通过 `onnxruntime-genai` 运行时运行、适配 Ryzen AI 软件 1.7.1 OGA 的模型配置,均支持上下文缓存复用。这包括:
- 混合模型:工作负载在 CPU、iGPU 和 NPU 之间进行分配
- NPU-TPS 融合模型:工作负载完全在 NPU 上融合
开始体验
按照官方 [安装说明] (https://ryzenai.docs.amd.com/en/latest/inst.html) 安装 Ryzen AI 软件 1.7.1。安装程序将创建一个 Conda 环境 (`ryzen-ai-1.7.1`),其中包含所有必需的依赖项,包括 `onnxruntime-genai`。
下载并配置模型
本演练使用 AMD Qwen2.5-3B 混合模型。从 Hugging Face 下载模型:
[amd/Qwen2.5_3B_Instruct_rai_1.7.1_hybrid](amd/Qwen2.5_3B_Instruct_rai_1.7.1_hybrid at main)
将所有下载的文件保存到本地目录,例如“D:\model\qwen2.5-3B”。
在下一部分,我们将使用此模型与两个具体示例来演示 KV 缓存复用功能的用法。
构建多回合对话
本示例展示了如何通过运行提示词序列来展开多回合对话,其中每一回合都以前面的回合为基础,且模型会记住所有内容,而无需通过代码进行任何显式的上下文管理。
测试涵盖的内容
以下六个场景代表了在现实世界常见的交互模式,在这些场景中多回合记忆至关重要。每一行都是一个提示词序列 `DEFAULT_PROMPTS`:
场景 |
示例提示词序列
|
身份信息记忆 |
我叫 Alex,在 Orbital AI 担任数据科学家。 |
我住在柏林,空闲时喜欢弹钢琴。 |
|
我住在哪里,我喜欢做什么? 我的职业是什么? |
|
参考自定义规则 |
当我说“简要概括”时,请仅以 3 个要点作答。 |
请简要概括以下内容:古埃及(埃及语:km.t)是文明的发源地之一,主要位于非洲东北部尼罗河下游流域。按传统埃及纪年,古埃及文明起源于公元前 3150 年左右的史前埃及,当时美尼斯统一了上下埃及。多数埃及学家认为,美尼斯与纳尔迈是同一人。古埃及历经王朝更迭,不仅出现了一系列政权稳定的王国,其间还穿插着相对动荡的“中间时期”。这些稳定的王朝分属三个时代:青铜时代早期的古王国、青铜时代中期的中王国、青铜时代晚期的新王国。 |
|
请简要概括蝴蝶的各个生长阶段 |
|
实体关联 |
Sarah 是一名软件工程师,负责领导 AI 团队。James 是她的经理。 |
Sarah 的经理是谁? |
|
Sarah 领导哪个团队? |
|
清单内容记忆 |
我需要从商店购买以下物品:苹果、面包、牛奶、鸡蛋、番茄、意大利面和奶酪。 |
清单中的第四样物品是什么? |
|
清单中有多少种乳制品? |
|
实体关联:多人物角色追踪 |
Alice 是一名生物学家,就职于 Genomix Lab。她的直属上级是 Patel 博士,Patel 博士是实验室负责人。她的同事 Martin 专门负责数据分析。 |
Alice 的上级是谁? |
|
Genomix Lab 的数据分析师是谁? |
|
Patel 博士的职位是什么? |
|
Alice 从事哪个领域的工作? |
|
任务连续性:依赖上下文记忆的数学计算
|
一位农民有 3 块田地。每块田地有 240 棵苹果树。 |
如果每棵树能产 120 个苹果,那么单块田地能产多少个苹果? |
|
所有田地一共能产多少个苹果? |
|
| 如果有 10% 的苹果损坏,还有多少个苹果是好的? |
准备测试脚本
Ryzen AI 安装程序会随附一个参考脚本,位于:
C:\Program Files\RyzenAI\1.7.1\LLM\example\run_model.py
在同一目录下复制该脚本并命名为 `run_kvreuse.py`,并进行两项修改:
步骤 1 — 替换 `DEFAULT_PROMPTS`
以“身份信息记忆”场景作为起点:
DEFAULT_PROMPTS = [
" My name is Alex, I work as a data scientist at Orbital AI.",
" I live in Berlin and enjoy playing the piano in my free time.",
" Where do I live and what do I enjoy doing?",
" What is my profession?"
]
> 为什么使用前导空格?当某条生成内容因达到 token 限制而被截断时,最后一个 token 可能是一个不完整的字词(例如,“clas”而非“classical”)。如果直接附加下一个提示词,会导致拼接成一个格式错误的 token(“classMy name is...”),这会使模型产生混淆。前导空格会强制在生成的回复与下一个用户消息之间设置一个清晰的 token 边界。
步骤 2 — 替换 `generate_text()` 中的生成循环
```python
generator = og.Generator(model, params)
for i, prompt in enumerate(prompts):
# Append only the new prompt tokens — the generator retains all prior context
generator.append_tokens(tokenizer.encode(prompt))
loop = 0
while not generator.is_done() and loop < 120:
if loop == 0:
in_pos = generator.get_sequence(0) # mark start of this response
loop += 1
generator.generate_next_token()
# Extract only the tokens generated for this prompt
output_tokens = generator.get_sequence(0)[len(in_pos):]
output_text = tokenizer.decode(output_tokens)
print(f"\nPrompt #{i+1}: {prompt}")
print("Output:\n", output_text)
results.append({
"prompt": prompt,
"response": output_text,
"tokens": len(output_tokens)
})
total_tokens += len(output_tokens)
```
关键设计决策说明:
决策 |
原因 |
所有回合共用一个 `Generator` 实例 |
KV 缓存位于 Generator 内部。在各回合之间复用同一个 Generator 才能实现上下文累积;如果每个回合都创建一个新的 Generator,就会丢弃所有先前的上下文。 |
使用 `append_tokens()` 而非重新开始 |
这会向现有的 KV 缓存序列中添加新 token,而不是从头开始重新构建整个序列。 |
设置 `loop < 120` token 限制 |
防止模型生成过长的回复。此处使用较低的数值是为了加快测试速度;投入生产环境时,请调高该数值。 |
对于 `loop == 0`,执行 `in_pos = generator.get_sequence(0)` |
记录本回合开始生成回复时的序列长度,以便仅解码并输出新 token,而不输出整个对话历史记录。 |
运行测试
```bash
conda activate ryzen-ai-1.7.1
cd "C:\Program Files\RyzenAI\1.7.1\LLM\example"
python run_kvreuse.py -m D:\model\qwen2.5-3B
```
预期结果
提示 1 和提示 2 为模型提供了背景信息。提示 3 和提示 4 用于测试模型是否保留了相关信息:
提示 3 —“我住在哪里,我喜欢做什么?”
```
您提到您住在柏林,空闲时喜欢弹钢琴。听起来真是个美妙的城市和生活方式!柏林以其充满活力的艺术氛围、多元化的街区和丰富的文化遗产而闻名,非常值得探索和尽情感受。弹钢琴是很不错的爱好,我敢肯定,您充分享受自己的闲暇时光。关于个人兴趣或未来规划,您还有什么想分享的吗?我非常喜欢这座城市和它的文化,特别喜欢探索新的街区
```
提示 4 —“我的职业是什么?”
```
您提到您在 Orbital AI 担任数据科学家。太棒了!数据科学家是一份极具吸引力的职业,需要运用统计学和计算技术,从数据中提取洞察与知识。这是一个技术门槛高、适用面广的专业领域,可覆盖各行各业,解决各类问题。作为一名数据科学家,您很可能有机会处理大型数据集、开发预测模型,并帮助组织做出数据驱动的决策。您最近参与过哪些项目或应对过哪些挑战?我非常喜欢这座城市和它的文化
```
模型仅根据先前回合中的对话内容就能正确回答这两个问题,调用方未显式传递任何上下文信息。KV 缓存负责处理信息记忆。
回退到先前状态
`rewind_to()` API 允许您将 KV 缓存指针移回对话中的任意先前位置,并丢弃该位置之后的所有内容。这适用于以下情况:
- 用户希望将对话引向与先前回合不同的方向
- 智能体工作流程需要从一个共同的起点探索多个不同分支
- 中间生成的回复质量较差,您希望从一个干净的状态重新提问
回退的工作原理
```
Normal flow:
Q1 → A1 → Q2 → A2 → Q3 → A3
KV cache: [Q1][A1][Q2][A2][Q3][A3]
After rewind_to(len(Q1 + A1)):
KV cache: [Q1][A1] ← everything after A1 is discarded
Re-ask Q2:
KV cache: [Q1][A1][Q2][A2'] ← model is in the same state as after the original A1
If the model is deterministic: A2' == A2
```
准备回退测试脚本
复制 `run_kvreuse.py` 并命名为 `run_rewind.py`。将生成循环替换为:
```python
# Phase 1: Run Q1, Q2, Q3 in sequence to build up the full KV cache
for i in range(3):
generator.append_tokens(tokenizer.encode(prompts[i]))
loop = 0
while not generator.is_done() and loop < 120:
loop += 1
generator.generate_next_token()
if i == 0:
# Save the sequence length after Q1+A1 — this is our rewind target
output_tokens0 = generator.get_sequence(0)
if i == 1:
# Save A2's text so we can compare it after the rewind
output_tokens1 = generator.get_sequence(0)
output_text1 = tokenizer.decode(output_tokens1)
# Phase 2: Rewind to the position after Q1+A1
generator.rewind_to(len(output_tokens0))
# Re-ask Q2 from this rewound state
generator.append_tokens(tokenizer.encode(prompts[1]))
loop = 0
while not generator.is_done() and loop < 120:
loop += 1
generator.generate_next_token()
output_tokens1_rewind = generator.get_sequence(0)
output_text1_rewind = tokenizer.decode(output_tokens1_rewind)
# Phase 3: Verify the outputs match
if output_text1 == output_text1_rewind:
print("rewind test OK")
return
```
各阶段所执行的操作:
阶段 |
执行的操作 |
原因 |
第一阶段 |
按顺序运行 Q1→A1→Q2→A2→Q3→A3 |
构建包含三回合对话历史记录的 KV 缓存以模拟真实对话 |
rewind_to(len(output_tokens0)) |
将缓存指针重置到 A1 之后的位置 |
从缓存中丢弃 Q2、A2、Q3、A3;仅保留 Q1+A1 |
第二阶段 |
从回退状态重新提问 Q2 |
此时,模型在接收 Q2 之前只能读取 Q1+A1,就像是第一次收到 Q2 一样 |
第三阶段 |
比较原 A2 和新 A2 |
确认回退操作将模型完全还原到先前状态 |
运行回退测试
```bash
python run_rewind.py -m D:\model\qwen2.5-3B
```
预期输出
```
rewind test OK
```
相同的输出证实,`rewind_to()` 成功将模型恢复到处理完 Q1 和 A1 之后的状态;从模型的角度来看,所有中间回合的痕迹均已清除。
总结与后续步骤
缓存复用为您带来的好处
功能 |
API |
适用场景 |
持久多回合记忆 |
`append_tokens()`(在一个共用的 `Generator` 实例上调用) |
聊天机器人、文档问答、智能体助手 |
对话分支/撤销 |
`rewind_to(position)` |
纠正错误回合、多路径探索、智能体重试 |
最佳实践
1.每个会话都复用同一个 `Generator` 实例;不要每个回合都创建一个新实例
2.对每个新回合使用 `append_tokens()`,而非完整地重新构建提示
3.在每个提示前添加一个前导空格,避免出现 token 边界残缺问题
4.每回合结束后,保存 `get_sequence(0)` 返回的长度;如果稍后想要返回到该点,需要将其作为参数输入 `rewind_to()`
上下文缓存复用基准测试
为了量化分析缓存复用功能所带来的收益,我们设计了以下测试用例。
(在 AMD Strix-Halo、128G 内存环境下测试)
测试用例 1:不使用此功能
如果不使用此功能,则在多回合对话中,需要将之前的对话历史记录追加到下一个提示。
- 输入提示 1:2048 个 token
- 输出响应 1:256 个 token
- 所需的输入提示 2:256 个 token
但是,实际上第二个提示必须包含整个对话历史记录:
full_prompt2 = 输入提示 1 + 输出响应 1 + 输入提示 2
因此,full_prompt2 的输入长度变为 2560 个 token。
- 输出响应 2:128 个 token
该实验重复进行了 10 次,取平均值作为结果。
结果:
- 响应 1 延迟:约 8.49 秒
(输入长度 = 2048,输出长度 = 256) - 响应 2 延迟:约 5.78 秒
(输入长度 = 2560,输出长度 = 128)
测试用例 2:使用此功能
启用此功能时:
- 输入提示 1:2048 个 token
- 输出响应 1:256 个 token
- 输入提示 2:256 个 token
- 输出响应 2:128 个 token
该实验重复进行了 10 次,取平均值作为结果。
结果:
- 响应 1 延迟:约 8.47 秒
(与测试用例 1 相同) - 响应 2 延迟:约 4.42 秒
(输入长度 = 256,输出长度 = 128)
|
用例 1 |
用例 2 |
输入提示 1 |
2048 |
2048 |
输出响应 1 |
256 |
256 |
第 1 轮对话的时间 |
8.49 秒 |
8.47 秒 |
输入提示 2 |
2560(输入提示 1 + 输出响应 1 + 输入提示 2) |
256 |
输出响应 2 |
128 |
128 |
第 2 轮对话的时间 |
5.78 秒 |
4.42 秒 |
从上述测试结果可以看出,在该场景下启用此功能可使响应时间缩短约 1.36 秒(5.78 秒 - 4.42 秒),即不同长度所产生的 TTFT 差异。
如果系统提示长度显著增大,节省的时间将会更加可观。
以下是此测试所使用的代码片段。为了进行测试,您需要自行准备测试数据,即一个名为 benchmark_test.txt 的大型文本文件,并将其放置在与 Python 脚本相同的路径下。
```python
def load_prompts(prompt_input):
"""Loads prompts from a .txt file, a direct string, or falls back to default prompts."""
if prompt_input:
if os.path.exists(prompt_input):
with open(prompt_input, "r", encoding="utf-8") as f:
prompts = [line.strip() for line in f.readlines() if line.strip()]
if prompts:
return prompts
else:
# Treat as a direct string input
return [prompt_input]
print("Warning: Invalid or missing prompt input. Using default prompts.")
return DEFAULT_PROMPTS
def load_model_and_tokenizer(model_path, verbose=False):
"""Loads the ONNX model and tokenizer, determining the model type from the config."""
config_path = os.path.join(model_path, 'genai_config.json')
# Read the model type from the configuration file
with open(config_path, 'r') as config_file:
config = json.load(config_file)
model_type = config['model']['type']
if verbose:
print(f"Loading {model_type} model from {model_path}...")
model = og.Model(model_path)
tokenizer = get_tokenizer(model_path, model_type, model)
return model, tokenizer, model_type
def _tokens_to_list(tokens):
"""Normalize tokenizer output to a flat Python list for append_tokens."""
if isinstance(tokens, np.ndarray):
return tokens.flatten().tolist()
return list(tokens)
def _generate_up_to(generator, max_new_tokens):
"""Generate up to max_new_tokens; stop early if is_done. Returns count generated."""
generated = 0
while generated < max_new_tokens and not generator.is_done():
generator.generate_next_token()
generated += 1
return generated
def _new_tokens_since(generator, start_len):
return _tokens_to_list(generator.get_sequence(0))[start_len:]
def generate_text(model, tokenizer, prompts, model_type, args):
"""KV benchmark: slice text1/text2 from benchmark_test.txt tokens, then time 10 runs."""
TEXT1_LEN = 2048
TEXT2_LEN = 256
OUT1_MAX = 256 # max new tokens after text1
OUT2_MAX = 128
BENCH_LOOPS = 10
NEED_TOKENS = TEXT1_LEN + TEXT2_LEN
script_dir = os.path.dirname(os.path.abspath(__file__))
benchmark_path = os.path.join(script_dir, "benchmark_test.txt")
if not os.path.exists(benchmark_path):
raise FileNotFoundError(f"benchmark_test.txt not found: {benchmark_path}")
with open(benchmark_path, "r", encoding="utf-8") as f:
benchmark_text = f.read()
# --- Phase 1: tokenize file only; take fixed slices (no model input of full file) ---
all_tokens = _tokens_to_list(tokenizer.encode(benchmark_text))
print(
f"Slicing from {benchmark_path}: encoded {len(all_tokens)} tokens, "
f"using [{0}:{TEXT1_LEN}) + [{TEXT1_LEN}:{NEED_TOKENS}), ignoring the rest."
)
if len(all_tokens) < NEED_TOKENS:
raise RuntimeError(
f"benchmark_test.txt too short after encode: got {len(all_tokens)} tokens, "
f"need at least {NEED_TOKENS} ({TEXT1_LEN} + {TEXT2_LEN})."
)
text1 = all_tokens[0:TEXT1_LEN]
text2 = all_tokens[TEXT1_LEN:NEED_TOKENS]
print(f"text1 ready: {len(text1)} tokens | text2 ready: {len(text2)} tokens")
# --- Phase 2: one generator — text1 -> out1, then text2 -> out2 (KV retained) ---
bench_max_len = TEXT1_LEN + TEXT2_LEN + OUT1_MAX + OUT2_MAX + 256
bench_params = og.GeneratorParams(model)
bench_search = {
"do_sample": args.do_random_sampling,
"max_length": bench_max_len,
"min_length": args.min_length,
"top_p": args.top_p,
"top_k": args.top_k,
"temperature": args.temperature,
"repetition_penalty": args.repetition_penalty,
}
bench_params.set_search_options(**{k: v for k, v in bench_search.items() if v is not None})
bench_params.try_graph_capture_with_max_batch_size(1)
total_time_text1 = 0.0
total_time_text2 = 0.0
out1 = None
out2 = None
print(f"\n[Phase 2] KV-retained benchmark ({BENCH_LOOPS} iterations)...\n")
for i in range(BENCH_LOOPS):
generator = og.Generator(model, bench_params)
# text1 -> out1 (up to OUT1_MAX new tokens, stop if is_done early)
t0 = time.time()
generator.append_tokens(text1)
in_pos = len(_tokens_to_list(generator.get_sequence(0)))
_generate_up_to(generator, OUT1_MAX)
out1 = _new_tokens_since(generator, in_pos)
total_time_text1 += time.time() - t0
# text2 -> out2 (same generator, KV from text1 retained)
t0 = time.time()
generator.append_tokens(text2)
in_pos2 = len(_tokens_to_list(generator.get_sequence(0)))
_generate_up_to(generator, OUT2_MAX)
out2 = _new_tokens_since(generator, in_pos2)
total_time_text2 += time.time() - t0
print("\n--- Phase 2 results (single generator, text1 then text2) ---")
print(f"avg time text1 -> out1: {total_time_text1 / BENCH_LOOPS:.4f}s")
print(f"avg time text2 -> out2: {total_time_text2 / BENCH_LOOPS:.4f}s")
# --- Phase 3: text1 -> 256 out1; new generator -> text1+out1+text2 -> out2 ---
total_time_p3_text1 = 0.0
total_time_p3_combo = 0.0
p3_out1 = None
p3_out2 = None
print(f"\n[Phase 3] split-generator benchmark ({BENCH_LOOPS} iterations)...\n")
for i in range(BENCH_LOOPS):
# Step A: text1 -> exactly OUT1_MAX (256) new tokens
gen_a = og.Generator(model, bench_params)
t0 = time.time()
gen_a.append_tokens(text1)
pos_a = len(_tokens_to_list(gen_a.get_sequence(0)))
_generate_up_to(gen_a, OUT1_MAX)
p3_out1 = _new_tokens_since(gen_a, pos_a)
total_time_p3_text1 += time.time() - t0
# Step B: fresh generator, text1 + out1 + text2 -> out2
gen_b = og.Generator(model, bench_params)
t0 = time.time()
gen_b.append_tokens(text1 + p3_out1 + text2)
pos_b = len(_tokens_to_list(gen_b.get_sequence(0)))
_generate_up_to(gen_b, OUT2_MAX)
p3_out2 = _new_tokens_since(gen_b, pos_b)
total_time_p3_combo += time.time() - t0
print("\n--- Phase 3 results (text1->512; new gen; text1+out1+text2->out2) ---")
print(f"avg time step A text1 -> out1: {total_time_p3_text1 / BENCH_LOOPS:.4f}s")
print(f"avg time step B text1+out1+text2 -> out2: {total_time_p3_combo / BENCH_LOOPS:.4f}s")
```
支持的模型
以下模型来自 HuggingFace (AMD),并已针对此功能进行了全面测试。
混合模型 |
NPU 融合模型 |
CodeLlama-7b-Instruct-hf Llama-2-7b-chat-hf Llama-3.2-1B-Instruct Llama-3.2-3B-Instruct Meta-Llama-3.1-8B-Instruct Mistral-7B-Instruct-v0.2 Mistral-7B-Instruct-v0.3 Phi-3.5-mini-instruct Phi-3-mini-128k-instruct Phi-4-mini-instruct Phi-4-mini-instruct-awq-quant-onnx-hybrid Qwen-2.5_1.5B_Instruct Qwen2.5_3B_Instruct Qwen2.5-7B-Instruct Qwen2.5-Coder-1.5B-Instruct Qwen2.5-Coder-7B-Instruct smollm_hybrid
|
CodeLlama-7b-Instruct-hf_fusion Llama-2-7b-chat-hf_fusion Llama-3.2-1B-Instruct_fusion Llama-3.2-3B-Instruct_fusion Meta-Llama-3.1-8B-Instruct_fusion Mistral-7B-Instruct-v0.1_fusion Mistral-7B-Instruct-v0.2_fusion Mistral-7B-Instruct-v0.3_fusion Phi-3-mini-128k-instruct_fusion Phi-3-mini-4k-instruct_fusion Phi-3.5-mini-instruct_fusion Phi-4-mini-instruct_fusion Qwen-2.5_1.5B_Instruct_fusion Qwen2.5-7B-Instruct_fusion Qwen2.5-Coder-0.5B-Instruct_fusion Qwen2.5-Coder-1.5B-Instruct_fusion Qwen2.5-Coder-7B-Instruct_fusion Qwen2.5_3B_Instruct_fusion |
总结
KV 缓存复用技术通过保留模型内部注意力状态,仅处理新增 token,而非重新构建整个对话,从而帮助降低多回合对话大语言模型应用的延迟。该技术可与对话回退功能结合使用,提高 Ryzen AI PC 上聊天机器人、文档助手和智能体应用的响应速度。
借助 Ryzen AI 软件 1.7.1 版本中提供的连续解码 API,开发人员可以实现持久化的对话记忆,降低响应延迟,并在 Ryzen AI PC 上解锁更高效的本地 AI 体验。
在此基础上,您可以进一步拓展这些技术的应用:为生产工作负载调高生成长度限制;测试更多的多回合对话记忆场景;将 rewind_to() 与分支逻辑结合,以创建对话树探索工作流程;或者,将相同模式应用于 NPU 融合模型,实现全程在 NPU 上完成推理。随着对话长度增加、系统提示日益复杂,KV 缓存复用技术带来的性能优势将变得愈发重要。
下一步探索方向
将 Token 上限提升至 120 以上,以满足生产级响应质量的要求
- 尝试 [多回合对话表](#测试涵盖的内容) 中涵盖的其他五种场景
- 将 `rewind_to()` 与分支逻辑相结合,构建对话树探索器
- 将相同模式应用于 NPU-TPS 融合模型,实现全程在 NPU 上完成推理
资源