已关闭
[Performance]: msprof解析性能优化分析与实施指导报告 #100
Mrtutu创建于  7月12日关闭于  21 天前
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%。真正需要优化的是其子栈下的业务函数,例如 DataProcessor、ApiProcessor、TaskProcessor、JsonAssembler 等。

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 聚合一致性
适用范围 TaskProcessor、CommunicationInfoProcessor、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::AssembleData、CannAssembler::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 增加 ArrayItemGuard 或 WriteSeparatorIfNeeded,避免手工字符串拼接。
  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 利用率,降低线程切换和锁竞争
预期收益 中。收益依赖机器核数和数据集并发结构
难度 中
风险 调度器改动面大,可能改变执行顺序
适用范围 ExportManager、ProcessControl、各 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/GetLocalTime 和 HPFloat 构造/运算成本
火焰图证据 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。
  • ItemFunc 是 std::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::GetParseItem 和 unordered_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。
  • 对比 CommunicationTaskData 和 CommunicationOpData 行数、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::Run、TraceEvent::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 再写:

    • 当前 DurationEvent 的 ts_ 是 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. TaskData 和 CommunicationData 分片。
  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::FormatData 和 TaskProcessor::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成员
21 天前 issue状态由 TODO 改变为 DONE
MrtutuMrtutu成员
21 天前 关闭了 issue
ascend-robotascend-robot成员
21 天前 添加了label:resolved