已关闭
[Performance]: msprof解析性能优化分析与实施指导报告 #100
Mrtutu创建于  7月12日关闭于  5 天前
Mrtutu
Mrtutu成员
7月12日 创建

ccac302472e7452d9d0700bf1b8c1993.svg

提交提案之前,请先检索仓库内是否已有相同的提案,如已有请在同一提案中进行讨论。

性能优化具体描述

msprof 解析性能优化分析与实施指导报告

1. 报告目的

本文基于当前仓库中的 flame.svg perf 火焰图结果,以及对 msprof C++/Python 解析与导出链路的代码分析,整理后续性能优化的候选方向、预期收益、实现难度、详细方案、风险与验证方法。

本报告用于指导后续多轮性能优化,不直接等同于已经验证的收益结论。所有预期收益均为基于火焰图占比和代码结构的估算,最终必须通过同一数据集、同一命令、同一环境下的优化前后实测确认。

2. 输入与分析范围

2.1 输入资料

  • 火焰图:flame.svg
  • 关键代码路径:
    • analysis/csrc/application/export_manager.cpp
    • analysis/csrc/domain/data_process/ai_task/api_processor.cpp
    • analysis/csrc/domain/data_process/ai_task/task_processor.cpp
    • analysis/csrc/domain/data_process/ai_task/communication_info_processor.cpp
    • analysis/csrc/infrastructure/utils/time_utils.cpp
    • analysis/csrc/infrastructure/utils/hp_float.h
    • analysis/csrc/domain/services/parser/parser_item_factory.cpp
    • analysis/csrc/application/timeline/json_assembler.cpp
    • analysis/csrc/application/database/db_assembler.h
    • analysis/csrc/infrastructure/db/include/connection.h

2.2 重点分析范围

本报告聚焦 msprof 解析/导出链路中的以下性能问题:

  • 架构级并行、流水线和阶段 barrier 问题
  • 数据处理阶段的逐行格式化热点
  • 时间戳换算与高精度数值实现热点
  • JSON/DB 输出阶段的中间对象和二次搬运
  • parser 工厂与通信/API 等模块内可局部优化点

不包含:

  • 具体代码修改 diff
  • 跨芯片全路径正确性结论
  • 已经实测确认的优化后收益

3. 当前火焰图关键结论

3.1 主要热点概览

热点栈或函数 火焰图占比 说明
ExportManager::ProcessData -> DataProcessor::Run 43.83% export 数据处理主阶段,是最大业务热点入口
ApiProcessor::Process 20.00% API 数据处理主入口
ApiProcessor::FormatData 19.33% API 逐行格式化,包含时间换算、枚举转换、字段填充
TaskProcessor::ProcessSingleDevice/Process 12.22% task 数据处理
TaskProcessor::FormatData 11.30% task 逐行格式化
GetTimeFromHostCnt/GetTimeFromCnt 约 12.70% host/device counter 到时间戳换算
GetLocalTime 约 5%+ 本地时间换算
JsonAssembler::Run 10.23% timeline JSON 组装与写出
DBAssembler::Run 5.75% msprof.db 组装与写出
CommunicationInfoProcessor::Process 6.69% 通信数据处理
ParserItemFactory::GetParseItem 2.83% parser item 查找,存在整表复制问题
StarsSocParser::ParseData/ParseDataItem 2.91% stars soc 解析热点

3.2 火焰图解释注意事项

ThreadPool::Loop/AddTask 在火焰图中显示约 81%,但这是线程池 worker 的包含栈,不应直接理解为线程池本身消耗 81%。真正需要优化的是其子栈下的业务函数,例如 DataProcessorApiProcessorTaskProcessorJsonAssembler 等。

Python 主入口在火焰图中也有约 14% 的解释器栈,但结合既有基线报告和代码路径看,当前 export/parse 重热点主要在 C++ 侧。Python 更像是调度入口或调用 C++ 扩展/子流程,不是本轮最大优先级。

4. 当前解析与导出链路架构

4.1 当前主流程

当前 export 侧主要是阶段式流程:

Python CLI / py_interface
  -> ExportManager::Run
      -> ExportManager::ProcessData
          -> HashInitProcessor
          -> ThreadPool 并行执行 DataProcessor
              -> ApiProcessor / TaskProcessor / CommunicationInfoProcessor / ...
              -> 结果写入 DataInventory
      -> DBAssembler::Run
          -> 从 DataInventory 取数据
          -> 转换成 vector<tuple<...>>
          -> SQLite 批量插入
      -> TimelineManager::Run
          -> JsonAssembler::Run
          -> 从 DataInventory 取数据
          -> 构造 TraceEvent 对象
          -> rapidjson 序列化
      -> SummaryManager::Run
          -> 从 DataInventory 聚合 summary

4.2 当前架构的主要问题

  1. 全局阶段 barrier 明显

    • ProcessData 必须完成后,DB/Timeline/Summary 才开始。
    • DB 写入、JSON 序列化、summary 聚合无法和前面的 parse/format 阶段重叠。
  2. DataInventory 作为全局中间仓库导致内存和搬运成本偏高

    • DataProcessor 先构造完整 vector。
    • 后续 DBAssembler 再转换成 vector<tuple<...>>
    • TimelineAssembler 再构造 TraceEvent 对象。
  3. 并行粒度偏 processor 级

    • 当前 ExportManager::ProcessData 基于 processor list 并行。
    • 对单个超重 processor,例如 API、Task、Communication,如果内部数据量很大,仍可能成为长尾。
  4. 输出阶段启动太晚

    • SQLite 插入和 JSON 写出属于可以和 CPU 格式化重叠的工作。
    • 当前却在 DataProcessor 完成后才开始。
  5. 一些 summary 逻辑本质可以 streaming 聚合

    • 不一定需要全量明细全部落入 DataInventory 后再统计。
    • 可以边读边聚合,结束时 finalize。

5. 架构级优化项

A1. 将阶段式处理改为流水线 producer-consumer

维度 内容
优化项 解析/格式化/DB 写入/JSON 生成/Summary 聚合流水线化
目标 用输出阶段和聚合阶段掩盖解析与格式化阶段的 CPU 时间,同时降低 DataInventory 内存峰值
预期收益 高。端到端 wall time 预计可下降 10% 到 30%,具体取决于 DB/JSON 与 FormatData 的重叠程度
难度
风险 数据顺序、错误传播、资源释放、跨消费者背压处理复杂
适用范围 API、Task、Communication、system timeline、summary 聚合等大数据流

当前瓶颈

当前形态:

DataProcessor 全量完成
  -> DataInventory 全量持有
      -> DBAssembler
      -> JsonAssembler
      -> SummaryAssembler

这种形态导致 CPU 格式化、JSON 序列化、SQLite 写入基本串阶段执行。

目标形态

Reader/Parser
  -> Formatter(batch)
      -> DBWriter(batch)
      -> TimelineWriter(batch)
      -> SummaryAggregator(batch)

详细方案

  1. 引入 Batch<T> 概念:

    template <typename T>
    struct DataBatch {
        std::vector<T> rows;
        uint16_t deviceId;
        bool eof = false;
    };
    
  2. 为每类核心数据建立 bounded queue:

    DataChannel<ApiData>
    DataChannel<AscendTaskData>
    DataChannel<CommunicationTaskData>
    
  3. DataProcessor 不再一次性把全部数据写入 DataInventory,而是按 batch 推送:

    LoadData batch -> FormatData batch -> Publish(batch)
    
  4. DB writer 独立消费 batch:

    ApiData batch -> bind -> sqlite step
    
  5. Timeline writer 独立消费 batch:

    ApiData batch -> write trace JSON fragment
    
  6. Summary aggregator 独立消费 batch:

    TaskData batch -> update aggregate state
    EOF -> finalize summary
    
  7. 使用 bounded queue 控制内存:

    • queue size 可按数据类型配置,例如 2 到 8 个 batch。
    • writer 跟不上时 producer 自动阻塞,避免内存无限增长。
  8. 错误传播:

    • 任一 consumer 失败时设置 shared cancel token。
    • producer 看到 cancel 后停止读取。
    • 所有 queue 发送 EOF,主调度器收敛错误。

分阶段落地建议

第一阶段不要直接重写全链路。建议先选择 ApiData 做试点:

ApiProcessor::FormatData
  -> 保留原 DataInventory 写入
  -> 额外增加可选 batch callback
  -> DB/Timeline writer 先做旁路验证

确认输出一致和收益后,再逐步推广到 Task 和 Communication。

A2. 按 device/table 分片并行,减少 processor 长尾

维度 内容
优化项 将 processor 级并行细化为 device/table shard 级并行
目标 提高多 device/多表场景并行度,减少单 processor 长尾
预期收益 中到高。多 device 大数据集预计 5% 到 20%;单 device 数据收益有限
难度
风险 合并顺序、全局 id、summary 聚合一致性
适用范围 TaskProcessorCommunicationInfoProcessor、system processors

当前瓶颈

例如 TaskProcessor::Process 内部按 device 遍历:

auto deviceList = Utils::File::GetFilesWithPrefix(profPath_, DEVICE_PREFIX);
for (const auto& devicePath: deviceList) {
    flag = ProcessSingleDevice(devicePath, allProcessedData) && flag;
}

对于多 device 数据集,单 processor 内部仍可能串行。

详细方案

  1. 抽象 shard 任务:

    TaskProcessor(device_0)
    TaskProcessor(device_1)
    CommunicationProcessor(device_0)
    CommunicationProcessor(device_1)
    
  2. 每个 shard 只产生本 device 的结果:

    vector<AscendTaskData> shardResult
    vector<CommunicationTaskData> shardResult
    
  3. 最后统一 merge:

    • 如果输出不依赖全局顺序,可直接 append。
    • 如果依赖时间顺序,按 (timestamp, deviceId, streamId, taskId) 排序。
    • 如果依赖全局 id,集中分配 id 或分段预分配 id range。
  4. 控制并行度:

    • 不建议每个 processor 每个 device 无限开线程。
    • 使用全局 scheduler 控制并发,避免 CPU oversubscription。

A3. 从固定阶段调度演进为 DAG 调度

维度 内容
优化项 用依赖 DAG 替换粗粒度阶段 barrier
目标 依赖满足的节点立即执行,不等待整个上一阶段完成
预期收益 中到高。大数据集和多输出模式预计 10%+
难度
风险 调度器复杂度、依赖声明准确性、故障恢复
适用范围 全 export 流程

目标 DAG 示例

HashInit
  -> ApiData
      -> DB:CANN_API
      -> Timeline:CANN
      -> Summary:API
  -> AscendTask
      -> DB:TASK
      -> Timeline:Task
      -> Summary:TaskTime
  -> Communication
      -> DB:COMMUNICATION_TASK
      -> DB:COMMUNICATION_OP
      -> Timeline:HCCL
      -> Summary:Comm

详细方案

  1. 定义节点类型:

    • SourceNode:读取原始 DB/bin。
    • TransformNode:格式化和转换。
    • SinkNode:DB/JSON/CSV 输出。
    • AggregateNode:summary 聚合。
  2. 定义依赖:

    node.name
    node.inputs
    node.outputs
    node.requiredExportModes
    node.canStream
    node.canShardByDevice
    
  3. 调度规则:

    • 所有输入 ready 或订阅 channel 后启动。
    • streaming 节点可以在上游未 EOF 时启动。
    • batch 节点必须等所有输入 ready。
  4. 先以配置表方式定义 DAG,不建议一开始做过度抽象。

A4. DB writer 后台化并支持 batch/iterator 直接写

维度 内容
优化项 DB 写入从 DBAssembler 阶段提前为后台 writer
目标 掩盖 SQLite bind/step/commit 和 tuple 转换成本
预期收益 中。DB 导出占 5.75%,流水线重叠后端到端收益取决于重叠程度
难度
风险 SQLite 单连接线程安全、事务边界、失败回滚
适用范围 所有 DB 输出表

当前代码特征

SaveData 当前接受 vector<tuple<...>>

template<typename... Args>
bool SaveData(const std::vector<std::tuple<Args...>> &data, const std::string &tableName, DBInfo& msprofDB)

SQLite 插入已经在事务中执行:

sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr);
...
sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr);

因此重点不是“一行一事务”,而是:

  • DBAssembler 前置构造了一份 vector<tuple<...>>
  • DB 阶段启动太晚

详细方案

  1. 给 DB 层增加 iterator/adapter 版本:

    template <typename Iter, typename Binder>
    bool InsertRows(const std::string& tableName, Iter begin, Iter end, Binder binder);
    
  2. 对 DataInventory 中的数据直接 bind:

    vector<ApiData> -> binder(ApiData) -> sqlite3_bind_*
    
  3. 流水线阶段:

    batch<ApiData> -> DBWriter queue -> transaction insert
    
  4. 按表管理事务:

    • 大表:每 N 行 commit 一次,降低超大事务内存和锁时间。
    • 小表:一次 commit。
  5. writer 线程模型:

    • SQLite 单 DB 建议单 writer 线程。
    • 多 DB 或临时 DB 可按 DB 文件拆 writer。

A5. Timeline JSON 流式写,减少 TraceEvent 对象池

维度 内容
优化项 Timeline 直接从数据 batch 写 JSON,减少 shared_ptr<TraceEvent> 中间对象
目标 降低 JSON 输出阶段 CPU、内存和分配成本
预期收益 中到高。JsonAssembler::Run 占 10.23%,大 timeline 数据集可能收益明显
难度 中到高
风险 JSON 格式一致性、逗号处理、metadata 顺序
适用范围 CANN/HCCL/Task/System timeline 输出

当前瓶颈

典型路径:

DataInventory data
  -> GenerateXXXTrace
  -> vector<shared_ptr<TraceEvent>> res_
  -> node->DumpJson(ostream)

HcclAssembler::AssembleDataCannAssembler::AssembleData 都会先生成事件对象,再写 JSON。

详细方案

  1. 增加直接写函数:

    void WriteApiTraceEvent(JsonWriter& writer, const ApiData& data, ...);
    void WriteHcclTraceEvent(JsonWriter& writer, const CommunicationTaskData& data, ...);
    
  2. 元数据保留现有对象方式或改成 direct writer。

  3. 对大规模 duration/counter/flow event 直接写:

    for row in batch:
        writer.StartObject()
        writer["name"] << ...
        writer["pid"] << ...
        writer["tid"] << ...
        ...
        writer.EndObject()
    
  4. 统一逗号和数组边界:

    • 当前通过额外写 "," 连接多个 assembler。
    • 建议 JsonWriter 增加 ArrayItemGuardWriteSeparatorIfNeeded,避免手工字符串拼接。
  5. 大文件输出建议支持分片临时文件:

    timeline.part.0.json
    timeline.part.1.json
    final merge
    

    这样 writer 可以边消费边落盘,而不是全部放在 StringBuffer

A6. Summary streaming 聚合

维度 内容
优化项 summary 从全量明细后处理改成边读边聚合
目标 降低 DataInventory 明细保存和二次遍历成本
预期收益 中。对 summary-only/export summary 场景更明显
难度
风险 聚合逻辑正确性、跨 device 合并、排序依赖
适用范围 API 统计、task time、通信统计、op summary 等

详细方案

  1. 定义 aggregator:

    class SummaryAggregator {
    public:
        void Update(const DataBatch<AscendTaskData>& batch);
        void Finalize(DataInventory& output);
    };
    
  2. 对只需要聚合值的 summary,不再保留全部明细。

  3. 对确实需要排序或 join 的 summary,保留必要索引,不保留完整原始对象。

  4. 对 export mode 做懒计算:

    • summary-only 时不构造 timeline-only 事件。
    • db-only 时不构造 summary-only 中间态。

A7. 全局线程池与背压调度

维度 内容
优化项 避免多层 ThreadPool 嵌套导致 oversubscription,统一管理并发
目标 稳定 CPU 利用率,降低线程切换和锁竞争
预期收益 中。收益依赖机器核数和数据集并发结构
难度
风险 调度器改动面大,可能改变执行顺序
适用范围 ExportManagerProcessControl、各 processor 内部并行

详细方案

  1. 引入全局 ExecutionContext

    maxCpuWorkers
    maxIoWorkers
    maxDbWriters
    cancellationToken
    memoryBudget
    
  2. CPU stage、I/O stage 分池:

    • CPU pool:format、parse、aggregation。
    • I/O pool:文件读取、JSON flush。
    • DB writer:通常单 writer。
  3. shard 和 processor 任务都提交到同一个 scheduler。

  4. bounded queue 提供自然背压。

6. 模块级优化项

M1. 时间换算 fast path,减少 HPFloat 热点

维度 内容
优化项 用整数/定点时间换算替代热路径 HPFloat
目标 降低 GetTimeFromHostCnt/GetTimeFromCnt/GetLocalTimeHPFloat 构造/运算成本
火焰图证据 GetTimeFromHostCnt/GetTimeFromCnt 约 12.7%,GetLocalTime 约 5%+,HPFloat 构造/赋值/加法多处进入 Top 热点
预期收益 高。端到端预计 5% 到 15%,API/Task/Communication 局部收益可能更高
难度
风险 时间精度、舍入规则、溢出、跨芯片 counter 规则
优先级 P0

当前问题

当前时间换算返回 HPFloat

HPFloat GetTimeFromCnt(uint64_t sysCnt, uint64_t hostMonotonic, uint64_t referenceCnt, double frequency)
{
    uint64_t timeDiff = ...;
    HPFloat res = static_cast<double>(timeDiff) / frequency;
    res = res << NS_US;
    res = HPFloat(hostMonotonic) +/- res;
    return res;
}

HPFloat 模板构造函数会初始化 30 位 vector,并通过 std::to_string 进入字符串化和逐位处理:

for (int32_t i = 0; i < defaultPrecision_; i++) {
    num_.emplace_back(0);
}
*this = value;

在 API/Task/Communication 的逐行循环中,这个成本会被放大。

详细方案

  1. 新增轻量时间转换器:

    class FastTimeConverter {
    public:
        explicit FastTimeConverter(const ProfTimeRecord& record, const SyscntConversionParams& params);
        uint64_t HostCntToLocalNs(uint64_t syscnt) const;
        uint64_t SyscntToLocalNs(uint64_t syscnt) const;
        uint64_t MonotonicToLocalNs(uint64_t timestamp) const;
    private:
        uint64_t baseTimeNs_;
        uint64_t hostMonotonic_;
        uint64_t hostCnt_;
        uint64_t sysCnt_;
        double hostFreq_;
        double freq_;
    };
    
  2. 对默认频率场景走纯整数:

    if hostFreq == DEFAULT_FREQ:
        return syscnt + baseTimeNs
    
  3. 对非默认频率使用定点换算:

    deltaNs = round((syscnt - referenceCnt) * 1000 / frequency)
    localNs = hostMonotonic +/- deltaNs + baseTimeNs
    
  4. 为避免 double 精度风险,可使用 long double__int128

    uint64_t deltaNs = static_cast<uint64_t>(
        std::llround(static_cast<long double>(diff) * 1000.0L / freq));
    
  5. 保留原 HPFloat API:

    • 先新增 fast API。
    • 热路径 processor 逐步迁移。
    • 对高风险路径保留 fallback。
  6. 建立回归测试:

    • 默认频率场景。
    • syscnt 大于 reference。
    • syscnt 小于 reference。
    • 接近 UINT64_MAX 的边界。
    • 与旧 HPFloat 结果逐条对比,允许明确的舍入误差范围。

首批迁移目标

  • ApiProcessor::FormatData
  • TaskProcessor::FormatData
  • CommunicationInfoProcessor::Update
  • CommunicationInfoProcessor::FormatData
  • MsprofTxHostProcessor
  • DpuProcessor
  • StepTraceProcessor
  • system processors 中高频 GetTimeFromHostCnt/GetTimeFromSyscnt/GetLocalTime 调用点

M2. 修复 ParserItemFactory 整表复制

维度 内容
优化项 GetParseItem 使用引用查找,不复制注册表和内层 map
目标 降低 parser item 查找中的 unordered_map/std::function 分配和复制
火焰图证据 ParserItemFactory::GetParseItem 约 2.83%,并伴随 unordered_map<ParserType,...std::function...> 分配热点
预期收益 中。局部收益明显,端到端预计 1% 到 3%
难度
风险
优先级 P0

当前问题

当前实现:

std::unordered_map<ParserType, ItemFuncMap> parserItemFuncs = GetContainer();
auto it = parserItemFuncs.find(parserType);
...
auto parserFunc = it->second;
auto ans = parserFunc.find(itemType);

问题:

  • GetContainer() 返回静态 map 引用,但赋值给局部变量会复制整个 map。
  • auto parserFunc = it->second 又复制内层 map。
  • ItemFuncstd::function,复制成本更高。

详细方案

改为引用:

const auto& parserItemFuncs = GetContainer();
auto it = parserItemFuncs.find(parserType);
...
const auto& parserFunc = it->second;
auto ans = parserFunc.find(itemType);

同时可考虑返回 const ItemFunc*const ItemFunc& 避免返回值复制,但这会改变接口,建议作为第二步。

验证方法

  • 跑 parser item 相关 UT。
  • 用 perf 对比 ParserItemFactory::GetParseItemunordered_map/std::function 分配栈是否下降。

M3. API ModelLoad 过滤从线性扫描改为按 threadId 区间索引

维度 内容
优化项 优化 ApiProcessor 中 ModelLoad 范围过滤
目标 避免每条 API 数据线性扫描所有 ModelLoad 数据
预期收益 中。取决于 API 数据量和 ModelLoad 数量
难度 低到中
风险 过滤语义变化风险,需要结果对比
优先级 P1

当前问题

当前逻辑:

for (const auto& data : processedData) {
    if (data.apiName == "ModelLoad") {
        modelLoadDatas.push_back(data);
    }
}

FilterDataByStartTime<ApiData>(processedData, record.startTimeNs, PROCESSOR_NAME_API,
    [modelLoadDatas](const ApiData& data, uint64_t startTimeNs) {
        bool inRange = isContainedInRange(modelLoadDatas, data);
        ...
    });

isContainedInRange 对每条 data 遍历全部 modelLoadDatas,并且 lambda 按值捕获 modelLoadDatas 会复制一次 vector。

详细方案

  1. 建立按 threadId 分组的区间:

    std::unordered_map<uint64_t, std::vector<Interval>> modelLoadRanges;
    
  2. 每个 threadId 下按 start 排序,合并重叠区间。

  3. 判断 data 是否落入区间时二分查找:

    O(log M_thread)
    
  4. lambda 捕获改为引用或外部函数对象。

  5. 如果 ModelLoad 数量很少,可设置阈值:

    if modelLoadDatas.size() < 16:
        keep current linear path
    else:
        build interval index
    

验证方法

  • 构造同 threadId、多 threadId、重叠区间、边界等 UT。
  • 对比过滤前后 processedData 数量和关键字段。

M4. CommunicationInfoProcessor 减少字符串 key 和重复 map 查找

维度 内容
优化项 优化通信数据处理中的 opKey、hash/enum 查找和 map 更新
目标 降低 CommunicationInfoProcessor 中字符串拼接、unordered_map 查找、HPFloat 转换成本
火焰图证据 CommunicationInfoProcessor::Process 6.69%,ProcessKfcData 6.21%,FormatKfcData 4.86%,Update 4.67%
预期收益 中。通信密集数据集收益明显
难度
风险 op 聚合 key 语义、通信大/小算子匹配正确性
优先级 P1

当前问题

热点逻辑包括:

  • 每条任务构造 taskData.opKey = Join("_", opName, groupName, deviceId)
  • opDataMap[taskData.opKey] 多次访问。
  • 每条任务做 GetGroupNameValue 和多个 GetEnumTypeValue
  • KFC/HCCL 两类数据多次扫描/更新。

详细方案

  1. map 预留容量:

    opDataMap.reserve(communicationData.oriTaskData.size() / estimatedTasksPerOp);
    endpoints.reserve(...);
    
  2. 使用 try_emplace

    auto [it, inserted] = opDataMap.try_emplace(taskData.opKey);
    if (inserted) {
        ...
    }
    
  3. 避免重复 operator[]

    当前多次 opDataMap[taskData.opKey] 会重复 hash/查找。应保存 iterator。

  4. groupName 映射缓存:

    std::unordered_map<std::string, std::string> groupNameCache;
    
  5. enum 映射缓存:

    rdmaType/transportType/dataType/linkType 这种重复字符串建立局部 cache。

  6. opKey 结构化:

    中期可将字符串 key 改为结构体:

    struct CommOpKey {
        std::string opName;
        std::string groupName;
        uint16_t deviceId;
    };
    

    或对字符串做 interning 后使用整数 id key。

  7. 时间转换纳入 M1 fast path。

验证方法

  • HCCL/KFC 原有 UT。
  • 对比 CommunicationTaskDataCommunicationOpData 行数、opKey、timestamp/end、relay/retry。
  • 对通信密集数据集做 perf 复测。

M5. TaskProcessor GetTaskType 与时间换算优化

维度 内容
优化项 TaskProcessor::FormatData 中时间换算 fast path,并减少 task type map 拷贝
目标 降低 task 数据逐行格式化成本
火焰图证据 TaskProcessor::FormatData 11.30%,GetTaskType 1.72%
预期收益 中到高。端到端 2% 到 8%,视 task 数据量而定
难度 低到中
风险 taskType 兼容性
优先级 P1

当前问题

TaskProcessor::FormatData 每条数据:

HPFloat start{tmpStart};
HPFloat end = start + HPFloat(data.duration);
data.timestamp = GetLocalTime(start, timeRecord).Uint64();
data.end = GetLocalTime(end, timeRecord).Uint64();
data.taskType = GetTaskType(...);

GetTaskType 内部:

std::map<std::string, std::string> sqeType;
if (Context::IsStarsChip(platformVersion)) {
    sqeType = STARS_SQE_TYPE_TABLE;
} else {
    sqeType = HW_SQE_TYPE_TABLE;
}

这里每次调用都可能复制 map。

详细方案

  1. 时间转换使用 M1 fast path。

  2. GetTaskType 中表改为 const 引用:

    const auto& sqeType = Context::IsStarsChip(platformVersion)
        ? STARS_SQE_TYPE_TABLE
        : HW_SQE_TYPE_TABLE;
    
  3. 对常见 (hostType, deviceType, platformVersion) 做小型缓存:

    std::unordered_map<TaskTypeKey, std::string> taskTypeCache;
    
  4. 如果 deviceType 是数字字符串,避免每次 IsNumber + map 查找,可缓存 deviceType -> resolvedType

M6. JsonWriter 与 Timeline 事件序列化优化

维度 内容
优化项 减少 JsonWriter 成员名写入、字符串转换和中间事件对象
目标 降低 JsonAssembler::RunTraceEvent::DumpJson、rapidjson 写字符串成本
火焰图证据 JsonAssembler::Run 10.23%,JsonWriter::operator[]/Member、rapidjson String/WriteString 多处上榜
预期收益 中。timeline 大数据集收益明显
难度
风险 JSON 输出一致性
优先级 P1

详细方案

  1. 大量事件直接写 JSON,不创建 TraceEvent 对象。

  2. 对固定字段名使用常量:

    constexpr const char* FIELD_NAME = "name";
    constexpr const char* FIELD_PID = "pid";
    
  3. 避免时间戳先转 string 再写:

    • 当前 DurationEventts_std::string
    • 可改为数值写出或延迟格式化。
  4. JsonWriter 支持 reserve:

    • 如果可预估输出大小,初始化 StringBuffer 时 reserve。
  5. 超大 JSON 支持 streaming file writer:

    • 当前通过 StringBuffer 全量持有,再 FlushToFile
    • 可使用 rapidjson FileWriteStream 或项目已有 DumpTool 分片写。
  6. ProcessArgs 里重复 key/value 的字段做直接写函数。

M7. DBAssembler 减少 tuple 中间层

维度 内容
优化项 从 DataInventory 对象直接 bind 到 SQLite,减少 vector<tuple> 构造
目标 降低 DB 输出阶段 CPU 和内存
火焰图证据 DBAssembler::Run 5.75%,SQLite bind/step 单点占比不高,说明中间转换也需关注
预期收益 中。DB-only 或大表导出收益更明显
难度
风险 DB 插入模板改动影响面广
优先级 P2

详细方案

  1. 对高频大表先做专用 direct saver:

    • TASK
    • COMMUNICATION_TASK_INFO
    • CANN_API
    • HCCL/Task timeline 相关表
  2. 保留通用 SaveData(vector<tuple>) 作为 fallback。

  3. 新增 adapter:

    struct ApiDataBinder {
        static constexpr auto TableName = TABLE_NAME_CANN_API;
        void Bind(Connection& conn, const ApiData& row) const;
    };
    
  4. 先减少一层 vector,后续再后台化。

M8. LoadData 查询裁剪与早过滤

维度 内容
优化项 在 SQL 查询阶段过滤不需要的数据或只查询必要字段
目标 减少 oriData 内存、tuple 构造和后续 FormatData 行数
预期收益 中。取决于数据可过滤比例
难度
风险 SQL 过滤条件和原过滤语义不一致
优先级 P2

示例

TaskProcessor::LoadData 已经有:

WHERE a.device_task_type != 'UNKNOWN'

可以进一步评估是否能在 SQL 中提前过滤 start time、source、level 等条件,减少进入 C++ 循环的数据。

注意

不要盲目把所有过滤都下推到 SQL。需要确认:

  • 原逻辑是否依赖转换后的 local time。
  • 是否需要保留 ModelLoad 区间内数据。
  • SQL 查询是否引入额外索引成本。

M9. DataInventory 内存与并发访问优化

维度 内容
优化项 对 DataInventory 做 shard 化、移动语义和生命周期释放优化
目标 降低内存峰值和 cache miss
预期收益
难度 中到高
风险 生命周期和消费者依赖
优先级 P2

详细方案

  1. 数据按 device 或 table shard 存储。

  2. 下游消费完成后及时释放:

    ApiData consumed by DB + Timeline + Summary
    -> release ApiData
    
  3. 对只读共享数据使用 shared_ptr<const vector<T>>

  4. 对单消费者数据使用 move。

  5. 为 DAG 节点声明数据生命周期,自动释放 no-longer-used data。

M10. ProcessControl/ThreadPool 开销治理

维度 内容
优化项 减少每层 process level 重复创建 ThreadPool,控制并发粒度
目标 降低线程创建/停止和调度开销
火焰图证据 RunPreparedProcess/Process::Run 约 15% 包含栈,需要结合子栈分析
预期收益 低到中
难度
风险 调度语义变化
优先级 P3

详细方案

  1. ProcessControl::RunProcesses 中每个 level 创建一个 ThreadPool。可改为复用全局 pool。

  2. 小任务数量较少时,避免创建线程池,直接串行执行。

  3. 与 A7 全局调度器合并。

7. 优化项优先级总表

ID 优化项 类型 预期收益 难度 风险 优先级 建议阶段
M2 ParserItemFactory::GetParseItem 去复制 模块级 1%-3% P0 第 1 轮
M1 时间换算 fast path 替代热路径 HPFloat 模块级 5%-15% P0 第 1-2 轮
M3 API ModelLoad 区间索引 模块级 1%-5% 低中 P1 第 2 轮
M5 TaskProcessor 时间和 task type 优化 模块级 2%-8% 低中 P1 第 2 轮
M4 CommunicationInfoProcessor key/cache 优化 模块级 2%-8% P1 第 2-3 轮
A5/M6 Timeline JSON 直接写/流式写 架构+模块 3%-10% 中高 P1 第 3 轮
A4/M7 DB writer 后台化/减少 tuple 架构+模块 2%-8% P2 第 3-4 轮
A2 device/table shard 并行 架构级 5%-20% P2 第 4 轮
A1 全链路 producer-consumer 流水线 架构级 10%-30% P2 第 5+ 轮
A3 DAG 调度 架构级 10%+ P3 长期
A6 Summary streaming 聚合 架构级 2%-10% P3 长期
M8 SQL 查询裁剪/早过滤 模块级 1%-5% P3 按数据验证
M9 DataInventory 生命周期释放 架构级 内存收益高 中高 P3 长期
M10 ThreadPool 复用与调度治理 架构级 低中 P3 长期

8. 推荐实施路线

第 0 阶段:建立可复现基线

目标:

  • 固定数据集、命令、环境和重复次数。
  • 记录 wall time、CPU、内存峰值、输出文件 checksum/行数。
  • 采集 perf 火焰图、阶段级 trace、关键模块耗时。

建议数据:

  • 至少 1 个当前火焰图对应数据集。
  • 至少 1 个小数据集用于快速功能回归。
  • 如果改动公共时间换算,至少覆盖 host/device、不同芯片或不同 frequency 场景。

第 1 阶段:低风险局部优化

候选:

  1. M2 ParserItemFactory 去复制。
  2. M5 中 GetTaskType map 引用优化。
  3. M3 lambda 捕获和 ModelLoad 小改。

目标:

  • 小 diff。
  • 快速验证 perf 上热点下降。
  • 建立后续优化的测试闭环。

第 2 阶段:时间换算 fast path

候选:

  1. 新增 FastTimeConverter
  2. 先迁移 ApiProcessor::FormatData
  3. 再迁移 TaskProcessor::FormatData
  4. 最后迁移 CommunicationInfoProcessor

目标:

  • 攻击最大热路径。
  • 每迁移一个 processor 都单独验证输出一致性。

第 3 阶段:高频业务 processor 优化

候选:

  1. API 区间索引。
  2. Communication key/cache。
  3. Task type cache。
  4. LoadData 查询裁剪。

目标:

  • 降低逐行处理 CPU。
  • 为后续 batch/streaming 改造打基础。

第 4 阶段:输出侧优化

候选:

  1. Timeline direct writer。
  2. JSON streaming file writer。
  3. DB direct binder。
  4. DB writer 后台化。

目标:

  • 降低 JSON/DB 阶段。
  • 减少中间对象和内存峰值。

第 5 阶段:架构级流水线与 DAG

候选:

  1. ApiData 单链路 producer-consumer 试点。
  2. TaskDataCommunicationData 分片。
  3. 统一 channel、backpressure、cancel token。
  4. 逐步替换 DataInventory barrier。

目标:

  • 真正实现 parse/format/export 的并行掩盖。
  • 降低总 wall time 和内存峰值。

9. 验证与度量方法

9.1 功能正确性

每轮优化必须验证:

  • UT 通过。
  • 相同输入下输出文件存在且结构一致。
  • 关键 DB 表行数一致。
  • 关键字段 checksum 或抽样一致。
  • timeline JSON 可被消费端打开或解析。

建议增加自动对比脚本:

compare_msprof_output.py
  --before baseline_output
  --after optimized_output
  --compare-db-tables
  --compare-json-structure
  --sample-rows 1000

9.2 性能指标

每轮至少记录:

指标 说明
wall time 端到端耗时,至少 3 次
CPU time 用户态/内核态 CPU
peak RSS 内存峰值
output size 输出文件大小
flame top diff 火焰图热点变化
module trace 关键模块耗时
row throughput 每秒处理行数

9.3 收益判定

建议规则:

  • 小优化:端到端收益 >= 1% 且热点函数明显下降,即可接受。
  • 中优化:端到端收益 >= 3%。
  • 大优化:端到端收益 >= 10%,且内存峰值不能显著恶化。

如果优化收益低于测试噪声,需要增加运行次数到 5 次或 10 次。

9.4 回归数据集

至少覆盖:

  • 小数据集:快速 UT/功能回归。
  • 中大型真实数据集:主要性能收益判断。
  • 通信密集数据集:验证 Communication 优化。
  • timeline 大数据集:验证 JSON 输出优化。
  • 多 device 数据集:验证 shard 并行。

10. 风险与控制策略

10.1 时间换算风险

风险:

  • HPFloat 可能用于规避 double 精度问题。
  • fast path 可能在极端 counter/frequency 下产生舍入差异。

控制:

  • 新旧双算对比。
  • 明确允许误差,例如 ns 级或 us 级,需要由产品语义确认。
  • 对默认频率和非默认频率分路径。
  • 首轮只迁移 API/Task,并保留 fallback。

10.2 流水线顺序风险

风险:

  • JSON 事件顺序变化。
  • DB 行插入顺序变化。
  • Summary 依赖全量排序。

控制:

  • 明确哪些输出要求稳定顺序。
  • 需要顺序的 sink 维持排序或按 shard merge。
  • 不要求顺序的 DB 表只验证内容一致。

10.3 并发与线程安全风险

风险:

  • DataInventory 不是为多 writer/多 reader streaming 设计。
  • SQLite 连接跨线程使用可能不安全。
  • JsonWriter 单实例不能多线程写。

控制:

  • 单 writer 原则。
  • queue 传递 ownership。
  • DataInventory 在长期方案中只保留最终产物或只读引用。

10.4 过度架构化风险

风险:

  • 一次性引入 DAG/channel/scheduler 改动过大。
  • 难以定位性能收益来源。

控制:

  • 先做低风险局部优化。
  • 流水线先选 ApiData 单链路试点。
  • 每轮只改 1 到 2 个热点,保留清晰 perf 对比。

11. 建议的首轮任务清单

首轮建议选择低风险且火焰图有明确证据的优化:

  1. 修复 ParserItemFactory::GetParseItem 整表复制。
  2. TaskProcessor::GetTaskType 使用 const 引用避免 map 拷贝。
  3. ApiProcessor 的 ModelLoad lambda 改引用捕获,并准备区间索引 UT。
  4. 增加 FastTimeConverter 原型和单测,但先不大规模迁移。

首轮完成标准:

  • 编译通过。
  • 相关 UT 通过。
  • flame 中 ParserItemFactory::GetParseItem 占比下降。
  • 端到端性能无回退。

12. 建议的第二轮任务清单

第二轮开始攻击最大热点:

  1. ApiProcessor::FormatData 迁移到 FastTimeConverter
  2. TaskProcessor::FormatData 迁移到 FastTimeConverter
  3. 建立输出一致性对比脚本。
  4. 用同一数据集跑优化前后 3 到 5 次。

第二轮完成标准:

  • ApiProcessor::FormatDataTaskProcessor::FormatData 火焰图占比明显下降。
  • GetTimeFromHostCnt/GetTimeFromCnt/GetLocalTime/HPFloat 相关热点下降。
  • 端到端 wall time 有可统计收益。

13. 长期目标架构

长期目标不是简单扩大线程池,而是形成以下结构:

Global Scheduler
  |
  +-- Source nodes
  |     +-- api_event.db reader
  |     +-- ascend_task.db reader
  |     +-- hccl_single_device.db reader
  |
  +-- Transform nodes
  |     +-- Api formatter shards
  |     +-- Task formatter shards
  |     +-- Communication formatter shards
  |
  +-- Sink nodes
        +-- DB writer
        +-- Timeline JSON writer
        +-- Summary aggregators

关键原则:

  • CPU stage 和 I/O stage 分离。
  • 大数据按 batch 传递。
  • 每个 channel 有背压。
  • 每个 sink 单独处理错误并可取消全局任务。
  • DataInventory 从“全局大仓库”演进为“最终结果/小型索引/兼容层”。

14. 总结

当前火焰图显示,msprof export/parse 的主要瓶颈集中在 C++ 数据处理与输出链路,尤其是:

  • API/Task/Communication 的逐行 FormatData
  • HPFloat 驱动的时间换算
  • 全局 DataInventory barrier 后的 JSON/DB 二次转换
  • parser item 查找中的不必要 map 复制

短期应优先处理确定性强、风险低、收益可验证的模块优化,例如 ParserItemFactory 去复制和时间换算 fast path。中期应优化 API/Task/Communication 的逐行处理和 JSON/DB 输出。长期则应将当前阶段式架构演进为 batch streaming + DAG scheduler,通过并行掩盖和内存生命周期治理获得更大的端到端收益。

性能劣化说明

其他相关讨论

环境信息

例如:
- 操作系统
- 昇腾硬件信息
- CANN软件版本
- 安装的对应软件版本

欢迎加入社区,感谢您对社区的贡献 🎉!

likedislike
MrtutuMrtutu成员
7月12日 添加了label:performance
Mrtutu
Mrtutu成员
7月12日 评论:

👋 您好,欢迎向 MindStudio msprof 提交 Issue!
我们已收到您的反馈,感谢你对开源社区的支持。🎉

📅处理时效: 维护团队将在24小时内 查看并回复您的问题(工作日)。
🔍自助查询: 在等待期间,建议您先查阅以下资料,可能已有解决方案:

📖 MindStudio msprof官方文档
📝 贡献者指南

请确保 Issue 描述清晰,包含复现步骤和日志,这将帮助我们更快定位问题。谢谢!

likedislike
MrtutuMrtutu成员
7月12日 添加了label:triaged
MrtutuMrtutu成员
7月12日 关联了里程碑:MindStudio 26.2.0
MrtutuMrtutu成员
7月12日 修改了issue 的描述
WangJie
WangJie成员
7月17日 评论:

ScreenShot_20260717141841_compressed.PNG
当前 IdPool::GetId 的实现中,有大量int2string,以及字符串拼接操作,造成了极大的性能开销,优化这部分逻辑可以获得10%左右性能收益
使用tuple直接作为map的key,优化后结果
ScreenShot_20260717142251_compressed.PNG

likedislike
Wangang Yu
Wangang Yu成员
7月22日 评论:

/label add pending

likedislike
ascend-robotascend-robot成员
7月22日 添加了label:pending
MrtutuMrtutu成员
5 天前 issue状态由 TODO 改变为 DONE
MrtutuMrtutu成员
5 天前 关闭了 issue
ascend-robotascend-robot成员
5 天前 添加了label:resolved