1 简介
见【CANN训练营】Qwen3-8B 推理优化实践食用指南-CSDN博客
2 Baseline跑通
notebook地址:gitcode.com/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/02_baseline_inference.ipynb
2.1 环境与工作区准备
import json import os import subprocess import sys from pathlib import Path print('[环境准备] 开始', flush=True) os.environ.setdefault('QWEN3_8B_DOWNLOAD_BACKEND', 'modelscope') os.environ['TOKENIZERS_PARALLELISM'] = 'false' os.environ.setdefault('ASCEND_RT_VISIBLE_DEVICES', '0') def locate_repo_root(): repo_roots = [] for env_name in ('GITCODE_REPO_ROOT', 'CANN_LEARNING_HUB_ROOT', 'REPO_ROOT'): if os.environ.get(env_name): repo_roots.append(Path(os.environ[env_name])) try: cwd = Path.cwd() lineage = [cwd, *cwd.parents] repo_roots.extend(lineage) repo_roots.extend(base / 'cann-learning-hub' for base in lineage) for base in lineage: try: repo_roots.extend(base.glob('*/cann-learning-hub')) except OSError: pass except FileNotFoundError: pass repo_roots.append(Path('/opt/atomgit/cann-learning-hub')) seen = set() for repo_root in repo_roots: key = str(repo_root) if key in seen: continue seen.add(key) if (repo_root / 'tutorials/llm_inference/qwen3_8b/src').exists(): return repo_root raise FileNotFoundError('Cannot locate cann-learning-hub repository root. Open this notebook inside cann-learning-hub or set CANN_LEARNING_HUB_ROOT to the repository root.') REPO_ROOT = locate_repo_root() os.chdir(REPO_ROOT) TUTORIAL_DIR = REPO_ROOT / 'tutorials' / 'llm_inference' / 'qwen3_8b' SRC_DIR = TUTORIAL_DIR / 'src' SOURCE_DIR = REPO_ROOT / 'Sources' / 'model_inference_optimization' / 'qwen3_8b' SOURCE_DIR.mkdir(parents=True, exist_ok=True) if str(SRC_DIR) not in sys.path: sys.path.insert(0, str(SRC_DIR)) from inference_scripts.environment_check import find_unmet_requirements, read_pinned_requirements if not os.environ.get('ASCEND_TOOLKIT_HOME'): raise EnvironmentError('ASCEND_TOOLKIT_HOME is not set. Please activate the CANN environment before running this notebook.') cann_script = Path(os.environ['ASCEND_TOOLKIT_HOME']) / 'set_env.sh' env_cmd = f'source {cann_script} && env' env = subprocess.check_output(f"bash -lc '{env_cmd}'", shell=True, text=True, cwd=REPO_ROOT) for line in env.splitlines(): if '=' in line: os.environ.__setitem__(*line.split('=', 1)) REQUIREMENTS_FILE = TUTORIAL_DIR / 'requirements.txt' pinned_requirements = read_pinned_requirements(REQUIREMENTS_FILE) unmet_requirements = find_unmet_requirements(pinned_requirements) if unmet_requirements: print('[依赖检查] 以下依赖缺失或版本不匹配:') for name, actual, expected in unmet_requirements: print(f' - {name}: 当前={actual or "未安装"}, 要求={expected}') requirements_to_install = [f'{name}=={expected}' for name, _, expected in unmet_requirements] subprocess.run( [ sys.executable, '-m', 'pip', 'install', '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', '--extra-index-url', 'https://download.pytorch.org/whl/cpu', '--trusted-host', 'pypi.tuna.tsinghua.edu.cn', *requirements_to_install, ], check=True, ) remaining = find_unmet_requirements(pinned_requirements) if remaining: details = ', '.join(f'{name}: {actual or "未安装"} != {expected}' for name, actual, expected in remaining) raise RuntimeError(f'依赖安装后仍不满足统一版本:{details}') print('[依赖检查] 安装完成,统一环境已满足') else: print('[依赖检查] 所有依赖版本均满足,跳过安装') RECIPE_ROOT = SRC_DIR / 'inference_scripts' / 'recipe_qwen3_8b' RECIPE_CONFIG_DIR = RECIPE_ROOT / 'models' / 'qwen' / 'config' BASELINE_YAML_TEMPLATE = RECIPE_CONFIG_DIR / 'qwen3_8b_1tp.yaml' PROFILE_YAML_TEMPLATE = RECIPE_CONFIG_DIR / 'qwen3_8b_1tp_profile.yaml' OPTIMIZED_YAML_TEMPLATE = RECIPE_CONFIG_DIR / 'qwen3_8b_1tp_add_rmsnorm.yaml' INFER_SH = RECIPE_ROOT / 'executor' / 'scripts' / 'infer.sh' MODEL_ID = os.environ.get('QWEN3_8B_MODEL_PATH') or 'Qwen/Qwen3-8B' MODEL_SOURCE_HINT = 'local_path' if os.environ.get('QWEN3_8B_MODEL_PATH') else os.environ['QWEN3_8B_DOWNLOAD_BACKEND'] print('仓库目录 =', REPO_ROOT) print('教程目录 =', TUTORIAL_DIR) print('运行目录 =', SOURCE_DIR) print('recipes 目录 =', RECIPE_ROOT) print('YAML 目录 =', RECIPE_CONFIG_DIR) print('推理入口 =', INFER_SH) print('可见 NPU =', os.environ.get('ASCEND_RT_VISIBLE_DEVICES')) print('[环境准备] 完成', flush=True)这段代码是整个教程的环境准备阶段,核心目标是:把当前 Notebook 进程配置成“可以在昇腾 NPU 上跑 Qwen3-8B 推理”的完整环境。
执行对应单元格即可。
这部分代码完成了环境准备工作:先通过多策略定位并切换到教程仓库根目录,然后设置模型下载后端、tokenizer并行开关和可见NPU卡号等基础环境变量;接着通过执行CANN的set_env.sh脚本,把昇腾工具链所需的全部环境变量导入当前Notebook进程;随后读取requirements.txt并检查当前Python环境的依赖版本,若有缺失或不匹配则自动用清华镜像源安装补齐;最后定义recipes推理框架、YAML配置文件、推理入口脚本和模型ID等路径常量,并打印环境摘要供确认,为后续在昇腾NPU上运行Qwen3-8B推理和Profiling采集做好了完整的运行环境准备。
2.2 查看YAML并执行Baseline推理
2.2.1准备Baseline YAML
把 Baseline YAML 模板复制一份到运行目录,填入真实模型路径,并打印关键配置供确认。
import os import time from inference_scripts.recipe_workflow import ( collect_recipe_metrics, find_recipe_case_dir, prepare_runtime_yaml, print_yaml_focus, resolve_model_path, ) from inference_scripts.inference_qwen3_8b import ensure_recipe_log_ok BASELINE_WORK_DIR = SOURCE_DIR / 'run_outputs' / 'baseline_demo' BASELINE_YAML = SOURCE_DIR / 'recipe_yaml' / 'baseline_demo.yaml' BASELINE_WORK_DIR.mkdir(parents=True, exist_ok=True) print('[Baseline 推理] 开始', flush=True) resolved_model_path, model_source = resolve_model_path(str(MODEL_ID), quiet_model_io=False) prepare_runtime_yaml(BASELINE_YAML_TEMPLATE, BASELINE_YAML, model_path=resolved_model_path) print('[YAML 准备] Baseline 配置', flush=True) print('参考 YAML =', BASELINE_YAML_TEMPLATE) print('运行 YAML =', BASELINE_YAML) print_yaml_focus(BASELINE_YAML) baseline_log_dir = BASELINE_WORK_DIR / 'recipe_logs' baseline_log_dir.mkdir(parents=True, exist_ok=True) os.environ['QWEN3_RECIPE_ROOT'] = str(RECIPE_ROOT) os.environ['QWEN3_RUN_YAML'] = str(BASELINE_YAML) os.environ['QWEN3_RUN_LOG'] = str(baseline_log_dir / 'baseline_demo_infer_stdout.log') os.environ['QWEN3_RUN_START_TIME'] = str(time.perf_counter())什么是YAML
YAML(YAML Ain't Markup Language)是一种人类可读的配置文件格式,用缩进和键值对来表示数据,就像“简化版的 JSON,但不用写括号和逗号”。
为什么需要 YAML
这套教程里,YAML 的作用是:用文本描述“怎么跑模型推理”,而不需要改 Python 代码。
比如需要改模型路径、是否开 Profiler、是否开 RMSNorm 融合、生成长度这些内容,只需要改 YAML 文件,不用动任何 Python 代码。
YAML 长什么样
运行YAML目录文件地址在cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/recipe_yaml/baseline_demo.yaml(上一部分打印的内容)
作为Baseline配置,这份YAML的核心特征是关闭了各类性能优化(如 Profiler、RMSNorm 融合、缓存编译等),用于作为后续优化对比的“原始基准”。
YAML文件的主要字段及解释:
(1) model_config(模型与推理核心配置)
字段 | 当前值 | 含义 |
|---|---|---|
|
| 模型名称标识 |
|
| 模型权重实际路径(由代码自动填入) |
|
| 计算精度(BF16,兼顾性能与精度) |
|
| 执行模式(即时执行,非图模式) |
| false | 关键:关闭性能剖析(Baseline 不采集数据) |
|
| 启用检查点 |
| false | 关闭缓存编译 |
| false | 关闭静态内核 |
| 子字段 | 自定义优化开关(见下) |
custom_params(自定义优化,Baseline 全部关闭):
enable_npu_rmsnorm:false(关闭 NPU 原生 RMSNorm)enable_add_rmsnorm:false(关闭 Add+RMSNorm 融合,这是第4章要优化的核心)enable_prefetch:false(关闭预取)prefetch_size:18874368(预取大小,虽关闭但保留默认值)
(2)data_config(数据配置)
dataset:default(使用默认数据集)input_truncated_len:256(输入截断长度,最多 256 token)
(3)parallel_config(并行配置)
全部为1,表示单卡运行(无张量并行/专家并行):
world_size: 1attn_tp_size: 1(注意力张量并行)moe_tp_size: 1(MoE 张量并行)embed_tp_size: 1(嵌入层并行)lmhead_tp_size: 1(输出头并行)
(4)scheduler_config(调度与生成配置)
max_new_tokens:64(最多生成 64 个新 token)batch_size:1(单批次推理)enable_thinking:false(关闭思考模式,Qwen3 特有)
2.2.2 使用 recipes 命令启动推理
%%bash set -euo pipefail # 激活虚拟环境(如果有) source /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/venv/bin/activate # recipes 原生启动命令 echo 'recipes 启动命令:' echo "cd ${QWEN3_RECIPE_ROOT}" echo "bash executor/scripts/infer.sh --model qwen --mode offline --yaml ${QWEN3_RUN_YAML}" cd "${QWEN3_RECIPE_ROOT}" bash executor/scripts/infer.sh --model qwen --mode offline --yaml "${QWEN3_RUN_YAML}" 2>&1 | tee "${QWEN3_RUN_LOG}"这部分是 Baseline 推理的实际启动命令,核心作用是:通过 recipes 框架在昇腾 NPU 上真正跑一次 Qwen3-8B 推理。
注意:这部分需要在原代码开头加一行激活虚拟环境的代码(如果有)
recipes 是什么
recipes是昇腾 CANN 生态里的一套“大模型推理样例框架”,它不是模型本身,也不是 PyTorch,而是怎么在 NPU 上把某个大模型跑起来的工程封装。
infer.sh 是什么
qwen3_8b/src/inference_scripts/recipe_qwen3_8b/executor/scripts/infer.sh 是 recipes 的统一推理入口脚本。
主要任务:解析参数 → 加载环境 → 定位 YAML → 校验配置 → 调用launch函数真正在 NPU 上跑推理。
2.2.3 读取recipes运行结果
case_dir = find_recipe_case_dir(RECIPE_ROOT, BASELINE_YAML) if case_dir is None: raise RuntimeError(f'未找到 recipes 结果目录:{BASELINE_YAML.stem}') recipe_log_path = case_dir / 'log_0.log' ensure_recipe_log_ok(recipe_log_path) run_info = { 'stdout_path': os.environ['QWEN3_RUN_LOG'], 'stderr_path': os.environ['QWEN3_RUN_LOG'], 'wall_time_s': time.perf_counter() - float(os.environ['QWEN3_RUN_START_TIME']), 'recipe_case_dir': str(case_dir), 'recipe_log_path': str(recipe_log_path), } baseline_metrics = collect_recipe_metrics( RECIPE_ROOT, BASELINE_WORK_DIR, 'baseline_demo', BASELINE_YAML, run_info, model_id=str(MODEL_ID), resolved_model_path=resolved_model_path, model_source=model_source, optimization_mode='none', max_new_tokens=64, max_input_tokens=256, enable_thinking=False, ) print('[Baseline 推理] 完成', flush=True)这部分代码是Baseline推理的收尾阶段,核心作用是:定位推理结果目录,检查日志是否正常, 收集并保存推理指标,为后续分析和对比做准备。
参数含义:
参数 | 值 | 含义 |
|---|---|---|
|
| 本次是 Baseline(无优化) |
|
| 生成长度限制 |
|
| 输入长度限制 |
|
| 关闭思考模式 |
2.3 查看结果
print('[结果查看] 开始', flush=True) def status_cn(status): return {'ok': '通过', 'warning': '告警', 'failed': '失败'}.get(status, status) def yes_no(value): if value is None: return '未记录' return '是' if value else '否' def load_metrics(work_dir, tag): metrics_path = Path(work_dir) / 'metrics' / f'{tag}_summary.json' metrics = json.loads(metrics_path.read_text(encoding='utf-8')) print('指标文件 =', metrics_path) print('recipes YAML =', metrics.get('recipe_yaml_path')) print('recipes 结果目录 =', metrics.get('recipe_case_dir')) print('生成文件 =', metrics.get('outputs_path')) print('生成 token 数 =', metrics.get('generated_tokens')) print('总耗时(s) =', metrics.get('total_time_s')) print('吞吐(tokens/s) =', metrics.get('tokens_per_second')) rmsnorm_status = metrics.get('rmsnorm_status') or {} print('RMSNorm 融合 =', yes_no(rmsnorm_status.get('rmsnorm_fused'))) print('Add+RMSNorm 融合 =', yes_no(rmsnorm_status.get('add_rmsnorm_fused'))) sanity = metrics.get('generation_text_sanity') if sanity: print('吐字检查 =', status_cn(sanity.get('status')), f"警告={sanity.get('num_with_warnings')}", f"失败={sanity.get('num_failed')}") return metrics baseline_metrics = load_metrics(BASELINE_WORK_DIR, 'baseline_demo') print() print('生成文本:') for line in Path(baseline_metrics['outputs_path']).read_text(encoding='utf-8').splitlines(): if line.strip(): print(json.loads(line).get('generated_text', '')) print('[结果查看] 完成', flush=True)这段代码读取并展示刚才那次推理的完整指标和生成文本,便于直观确认 Baseline 状态。
运行结果:
[结果查看] 开始 指标文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/run_outputs/baseline_demo/metrics/baseline_demo_summary.json recipes YAML = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/recipe_yaml/baseline_demo.yaml recipes 结果目录 = /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/src/inference_scripts/recipe_qwen3_8b/models/qwen/res/20261013/qwen3_8b_baseline_demo 生成文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/run_outputs/baseline_demo/outputs/baseline_demo_generations.jsonl 生成 token 数 = 38 总耗时(s) = 5.3463899999999995 吞吐(tokens/s) = 7.10759970746616 RMSNorm 融合 = 否 Add+RMSNorm 融合 = 否 吐字检查 = 通过 警告=0 失败=0 生成文本: The output of an **attention function** is a **weighted sum of the value vectors**, where the weights are determined by the **similarity between the query vector and each key vector**. [结果查看] 完成打印内容 | 实际值 | 含义 |
|---|---|---|
| 路径 | 指标 JSON 的位置 |
| 运行 YAML 路径 | 用的是哪个配置 |
| 推理产物目录 | 日志、输出等存放位置 |
| outputs 路径 | 生成文本的 JSONL 文件 |
|
| 实际生成了 38 个 token |
|
| 推理总耗时 5.35 秒 |
|
| 每秒生成 7.11 个 token |
|
| 确认关闭 |
|
| 确认关闭 |
|
| 无告警、无失败 |
3 Profiling分析
这部分在Baseline的基础上打开torch_npu.profiler。
torch_npu.profiler 是 PyTorch 在昇腾 NPU 上的性能剖析器,它底层会调用 CANN / Ascend Profiling 能力,把三层数据对起来:
(1)Host 侧:Python 调用、PyTorch 算子下发、CPU 活动
(2)CANN 侧:算子编译、任务调度、内存拷贝、流同步
(3)NPU 侧:AI Core / AI CPU / Vector 核上的 kernel 执行时间、耗时、等待时间
它的目的不是“让模型更快”,而是:
先看清楚 Baseline 推理里,时间到底耗在哪些算子上了。
准备工作与运行
环境工作区准备的流程和baseline一致。准备YAML的时候准备的是Profiling YAML,在baseline的基础上打开Profiling。其余运行流程与baseline一致。
Profiling YAML中model_config.enable_profiler 设为true,其余推理参数保持和 Baseline 一致。
YAML 文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/recipe_yaml/baseline_profile.yaml model_path = /home/developer/.cache/modelscope/models/Qwen--Qwen3-8B/snapshots/master enable_profiler = True # 注意,这里的enable_profiler,即yaml里打开了profiler enable_npu_rmsnorm = False enable_add_rmsnorm = False enable_prefetch = False prefetch_size = None dataset = default input_truncated_len = 256 max_new_tokens = 64 batch_size = 1结果分析
[Profiler 产物] 开始 指标文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/run_outputs/baseline_profile/metrics/baseline_profile_summary.json 生成 token 数 = 38 Profiler 开启 = 是 Profile 目录 = /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/src/inference_scripts/recipe_qwen3_8b/models/qwen/res/20261013/qwen3_8b_baseline_profile/prof prof/prefill = /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/src/inference_scripts/recipe_qwen3_8b/models/qwen/res/20261013/qwen3_8b_baseline_profile/prof/prefill 一级目录 = 7f27e7db10fd4ffda8f0b031bf276588_11042_20261013190353199_ascend_pt kernel_details.csv = 已生成 trace_view.json = 已生成 prof/decode = /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/src/inference_scripts/recipe_qwen3_8b/models/qwen/res/20261013/qwen3_8b_baseline_profile/prof/decode 一级目录 = 7f27e7db10fd4ffda8f0b031bf276588_11042_20261013190359433_ascend_pt kernel_details.csv = 已生成 trace_view.json = 已生成 吐字检查 = 通过 警告=0 失败=0 Profiler 摘要文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/run_outputs/baseline_profile/profiler_summary/baseline_profile_summary.txt Profiler summary from kernel_details source: /mnt/workspace/gitCode/cann/cann-learning-hub/tutorials/llm_inference/qwen3_8b/src/inference_scripts/recipe_qwen3_8b/models/qwen/res/20261013/qwen3_8b_baseline_profile/prof/decode/7f27e7db10fd4ffda8f0b031bf276588_11042_20261013190359433_ascend_pt/ASCEND_PROFILER_OUTPUT/kernel_details.csv OP Type Count Total(us) Ratio(%) ---------------------------------------------------------------------------------------- aclnnMatmulWeightNz_MatMulCommon_MatMulV2 435 36531.740 68.653 aclnnMul_MulAiCore_Mul 978 3579.760 6.727 aclnnPowTensorScalar_PowsAiCore_Pows 435 2855.780 5.367 FusedInferAttentionScore 108 2413.320 4.535 aclnnMean_ReduceMeanAiCore_ReduceMean 435 2318.040 4.356 ... aclnnInplaceOne_OnesLikeAiCore_OnesLike 3 35.100 0.066 aclnnEmbedding_GatherV2AiCore_GatherV2 3 31.100 0.058 ... 仅显示前 24 行,共 35 行 [Profiler 产物] 完成[分析结论] 开始 摘要类型 = kernel_details 生成 token 数 = 38 Baseline RMSNorm 融合 = 否 Baseline Add+RMSNorm 融合 = 否 Top 算子: - aclnnMatmulWeightNz_MatMulCommon_MatMulV2: 次数=435,总耗时(us)=36531.740,占比=68.653% - aclnnMul_MulAiCore_Mul: 次数=978,总耗时(us)=3579.760,占比=6.727% - aclnnPowTensorScalar_PowsAiCore_Pows: 次数=435,总耗时(us)=2855.780,占比=5.367% - FusedInferAttentionScore: 次数=108,总耗时(us)=2413.320,占比=4.535% - aclnnMean_ReduceMeanAiCore_ReduceMean: 次数=435,总耗时(us)=2318.040,占比=4.356% - aclnnInplaceCopy_CastAiCore_Cast: 次数=873,总耗时(us)=1377.980,占比=2.590% - aclnnScatterNdUpdate_ScatterNdUpdateAiCore_ScatterNdUpdate: 次数=216,总耗时(us)=1090.060,占比=2.049% - aclnnAdds_AddAiCore_Add: 次数=441,总耗时(us)=625.340,占比=1.175% - aclnnRsqrt_RsqrtAiCore_Rsqrt: 次数=435,总耗时(us)=533.560,占比=1.003% - aclnnAdd_AddAiCore_Add: 次数=219,总耗时(us)=473.720,占比=0.890% 结论: - Baseline YAML 中 Dense RMSNorm 两个融合开关均为关闭,因此本章看到的是融合前的执行链路。 - 摘要中没有直接出现融合后的 RMSNorm 算子名,说明 Baseline 仍按小算子链路执行。 - 结合 QwenRMSNorm 的计算式,摘要中的归一化相关小算子包括:aclnnAdd_CastAiCore_Cast、aclnnArgMax_CastAiCore_Cast、aclnnGather_CastAiCore_Cast、aclnnInplaceCopy_CastAiCore_Cast、aclnnMax_ReduceMaxAiCore_ReduceMax、aclnnMean_ReduceMeanAiCore_ReduceMean、aclnnPowTensorScalar_PowsAiCore_Pows、aclnnRemainderTensorScalar_BroadcastToAiCore_BroadcastTo。 - 摘要中也可以看到残差 Add 相关算子:aclnnAdd_AddAiCore_Add、aclnnAdd_CastAiCore_Cast、aclnnAdds_AddAiCore_Add。 - 下一章保持 recipes 的 YAML + infer.sh 工作流,只切换 Dense RMSNorm NPU 融合开关,对比精度、吐字健康和吞吐变化。 分析文件 = /mnt/workspace/gitCode/cann/cann-learning-hub/Sources/model_inference_optimization/qwen3_8b/run_outputs/profiling_analysis_summary.json [分析结论] 完成算子耗时排名:
排名 | 算子 | 次数 | 耗时(us) | 占比 | 属于什么 |
|---|---|---|---|---|---|
1 | MatMul | 435 | 36531.74 | 68.65% | 线性层(QKV/O/FFN) |
2 | Mul | 978 | 3579.76 | 6.73% | RMSNorm 小算子 |
3 | Pow | 435 | 2855.78 | 5.37% | RMSNorm 小算子 |
4 | FusedInferAttentionScore | 108 | 2413.32 | 4.54% | 注意力融合算子 |
5 | ReduceMean | 435 | 2318.04 | 4.36% | RMSNorm 小算子 |
6 | Cast | 873 | 1377.98 | 2.59% | 类型转换 |
7 | ScatterNdUpdate | 216 | 1090.06 | 2.05% | KV Cache 更新 |
8 | Adds | 441 | 625.34 | 1.18% | 残差 Add |
9 | Rsqrt | 435 | 533.56 | 1.00% | RMSNorm 小算子 |
10 | Add | 219 | 473.72 | 0.89% | 残差 Add |
可以看出,MatMul(矩阵乘法)耗时最长,其余部分RMSNorm 被拆成了 8 个小算子,残差 Add 也是小算子没有融合。
Profiling 分析证实了 RMSNorm 未融合时被拆成 8 个小算子(Pow/ReduceMean/Rsqrt/Mul 等),导致launch 次数多、访存多、耗时高,是明确的性能瓶颈。下一章将打开enable_add_rmsnorm融合开关,用 A/B 对比验证优化效果。
trace_view.json 时间线图谱
trace_view.json时间线图谱,呈现了 Qwen3-8B 在昇腾 NPU 上Prefill与Decode两个阶段的执行特征。
Prefill:
对应prof/prefill,即处理输入提示词的阶段。
- 时间跨度与标记:整体约 150ms(50ms-200ms),标记为
ProfilerStep#0,末尾带有Dec 标记,暗示向 Decode 阶段的过渡。 - 算子形态:NPU(Stream 46/49)上的色块相对“大块”,虽然密集但多为一次性大粒度计算(如首轮的 QKV 大矩阵乘)。
- 重叠与频率:CPU(Python/CANN)与 NPU 有明显的调度交互。底部
Overlap Analysis中 Computing 与 Free 交替,由于仅执行一次,整体耗时较短,未暴露出小算子的反复累加效应。
Decode:
- 时间跨度与标记:整体约 350ms(100ms-450ms),包含
#3、#4、#5连续多步,每步对应一个(或几个)Token 的生成。 - 极度密集的细碎条纹:这是最核心的视觉特征。NPU 时间线上布满高频、细碎的彩色条纹,直观对应了前文数据中435次 MatMul 以及伴随的Pow/ReduceMean/Rsqrt/Mul(RMSNorm 未融合)等小算子。
- 多流并行:Stream 46 与 Stream 49 交错,CANN 线程持续调度,AI Core 频率满载(浅蓝色长条),说明计算单元持续忙碌。
对比维度 | Prefill | Decode | 性能含义 |
|---|---|---|---|
执行次数 | 单次(Step#0) | 多次迭代(Step#3/4/5) | Decode 重复放大了局部开销 |
算子粒度 | 相对大块(计算密集) | 极度细碎(访存/调度密集) | Decode 中小算子 Launch 开销显著 |
时间线密度 | 密集但无高频锯齿 | 满屏细碎条纹 | 印证 RMSNorm 拆分成 4~8 个小算子 |
耗时占比 | 较短(~150ms) | 较长(~350ms+) | 端到端吞吐受 Decode 阶段主导 |
图谱直观揭示了Decode阶段因 RMSNorm 未融合产生大量小算子调度(细碎条纹),是端到端性能的可优化瓶颈;Prefill 则相对健康。