开发者
资源
鲲鹏KUPL计算图
鲲鹏KUPL计算图
发表于03/03
9610

一、实验环境说明

1.1 硬件环境

本次实验采用鲲鹏920服务器集群核心节点,具体配置如下:服务器型号为Kunpeng 920-7260,部署2颗处理器,每颗处理器集成48个物理核心,核心主频2.2GHz,最高可睿频至2.6GHz,架构为aarch64(ARMv8-A)。内存配置为128GB DDR4 3200MHz高速内存,支持鲲鹏硬件多级内存架构(包括寄存器、L1/L2/L3缓存、主存的多级存储体系),其中每颗鲲鹏920处理器内置64MB L3缓存,每核配备64KB L1指令缓存+64KB L1数据缓存、512KB L2缓存,多级缓存架构可有效降低内存访问延迟,提升多核并行计算效率。存储方面配备1TB SSD固态硬盘,用于存储实验所需的安装包、代码文件及运行日志,满足高性能计算场景下的数据读写需求。

1.2 软件环境

本次实验软件环境均适配鲲鹏aarch64架构,具体配置及说明如下:

  • 操作系统:openEuler 22.03 LTS (aarch64),该系统是华为自主研发的开源操作系统,针对鲲鹏架构进行了深度优化,内置鲲鹏硬件加速引擎,支持多核并行调度、内存分级管理,稳定性和性能均适配高性能计算(HPC)场景,且提供完善的开源生态支持。
  • HPCKit 版本:鲲鹏HPCKit 2.3.0,是鲲鹏社区推出的高性能计算开发套件,集成了编译、调试、性能分析等一站式工具,内置KUPL库、并行计算示例代码及架构优化工具,可快速搭建鲲鹏HPC开发环境。
  • 编译器:毕昇编译器 3.1.0,基于LLVM架构开发,专为鲲鹏aarch64架构优化,支持C/C++/Fortran等编程语言,具备自动向量优化、多核并行优化、内存访问优化等功能,相比开源编译器(如GCC),在鲲鹏平台上可提升程序运行性能10%-30%。
  • 依赖库:① KUPL 库(鲲鹏统一并行编程框架),是鲲鹏平台专属的并行编程库,封装了计算图、多线程调度、内存管理等核心接口,简化鲲鹏多核并行开发难度,无需手动编写复杂的线程调度代码;② pthread库(POSIX线程库),用于底层线程创建与管理,KUPL库底层依赖该库实现多线程并行执行。
  • 开发工具:VSCode + Remote-SSH,VSCode提供完善的代码编辑、语法高亮、调试功能,通过Remote-SSH插件可远程连接鲲鹏服务器,实现本地编辑、远程编译运行,提升开发效率;辅助工具包括Git(用于获取开源示例代码)、htop(用于查看CPU、内存使用情况)、gdb(用于代码调试)。

二、实验原理介绍

2.1 计算图核心概念

计算图是高性能并行计算中常用的编程模型,用于将复杂的计算任务拆解为多个独立的子任务,通过定义子任务间的依赖关系,实现多线程/多进程并行执行。KUPL计算图针对鲲鹏多核架构进行了深度优化,采用“静态图(DAG有向无环图)+ 动态图”双层架构,兼顾任务调度的灵活性和并行执行的高效性,核心概念如下:

  • 静态图(DAG):核心作用是“定义任务”,将计算逻辑、数据移动、数据存储等操作封装为一个个“节点”,将子任务间的依赖关系(如A任务的输出是B任务的输入)封装为“边”,形成有向无环图(DAG),确保任务执行无循环依赖,避免死锁。静态图一旦定义完成,其节点和边的关系不再变化,适合预先明确任务流程的场景,可提前进行任务优化(如算子融合、内存复用)。
  • 动态图:核心作用是“调度任务”,负责解析静态图的结构,调度静态图中所有节点的并行执行,动态分配鲲鹏服务器的多核资源,实现多线程负载均衡。动态图支持实时调整任务执行顺序,可根据当前CPU核心负载情况,将任务分配给空闲核心,避免部分核心过载、部分核心空闲的情况,充分发挥鲲鹏920服务器96核(2颗×48核)的并行算力。
  • 节点与边:节点是计算图的基本执行单元,分为计算节点(执行具体的计算逻辑,如矩阵乘法、加法)、数据节点(用于存储计算数据、传递数据)、控制节点(用于控制任务执行顺序,如条件判断、循环控制);边分为数据依赖边(表示节点间的数据传递关系,如节点1的输出是节点2的输入)和控制依赖边(表示节点间的执行顺序关系,如节点1执行完成后,节点2才能开始执行)。
  • KUPL计算图优势:相比传统的OpenMP并行编程模型,KUPL计算图无需手动管理线程创建、任务分配和依赖关系,开发者仅需关注核心计算逻辑;针对鲲鹏硬件多级内存架构优化,支持数据在多级缓存间的高效移动,降低内存访问延迟;支持任务嵌套、迭代计算,适合高性能计算中常见的迭代求解、矩阵运算、数值模拟等场景。

2.2 KUPL计算图执行流程

KUPL计算图的执行流程遵循“定义-构建-调度-执行-释放”的核心逻辑,结合鲲鹏多核架构的特性,具体流程如下(以本次实验的E = A*B + C*D计算任务为例):

  • 静态图初始化:调用KUPL库的核心接口,创建一个空的静态图实例,指定静态图的配置参数(如节点最大数量、数据缓存大小),适配鲲鹏服务器的内存资源。
  • 添加任务节点:向静态图中添加4个计算节点(节点1:计算A*B,节点2:计算C*D,节点3:计算(A*B)+(C*D),节点4:输出计算结果E),以及2个数据节点(存储输入数据A、B、C、D和输出数据E),每个计算节点绑定对应的计算函数和输入输出数据。
  • 定义依赖关系:构建完整的DAG有向无环图,定义节点间的依赖关系:节点1和节点2为独立节点(无前置依赖,可并行执行);节点3的前置依赖是节点1和节点2(需等待节点1、节点2执行完成,获取其计算结果后,才能执行加法运算);节点4的前置依赖是节点3(需等待节点3计算完成后,输出最终结果E)。
  • 动态图初始化与任务提交:创建动态图实例,指定动态图的调度策略(如负载均衡调度、优先级调度),启用鲲鹏服务器的所有CPU核心(KUPL_ALL_EXECUTORS);将静态图封装为一个任务,提交至动态图,动态图开始解析静态图结构,分配线程资源。
  • 任务并行执行:动态图为节点1、节点2分配两个独立的线程,并行执行A*B和C*D的乘法运算(充分利用鲲鹏多核并行算力);待节点1、节点2执行完成后,动态图为节点3分配线程,执行加法运算;最后为节点4分配线程,输出计算结果E。
  • 阻塞等待与资源释放:调用阻塞接口,等待所有节点执行完成,确保计算结果的完整性;任务执行完毕后,依次销毁静态图、动态图实例,释放线程资源、内存资源,避免内存泄漏,符合鲲鹏高性能编程的资源管理规范。

该执行流程适合迭代计算、存在明确任务依赖的高性能计算场景(如矩阵运算、有限元分析、深度学习训练),通过任务并行执行,可大幅提升计算效率,相比单线程执行,计算速度可接近线性提升(理论上96核可提升96倍,实际受任务依赖、内存带宽等因素影响,提升倍数约为70-85倍)。

2.3 补充知识点:KUPL与鲲鹏架构的适配原理

KUPL库作为鲲鹏平台专属的并行编程框架,深度适配鲲鹏920服务器的硬件特性,核心适配原理如下:

  • 多核调度适配:KUPL动态图的调度算法针对鲲鹏920的48核/颗(2颗共96核)架构优化,支持线程与CPU核心的绑定(可通过API指定线程运行在特定核心上),避免线程在不同核心间频繁切换,降低上下文切换开销。
  • 内存架构适配:支持鲲鹏硬件多级内存架构,可通过KUPL API指定数据存储在L3缓存、主存中,优先将高频访问的数据存储在L3缓存中,降低内存访问延迟(L3缓存访问延迟约为10ns,主存访问延迟约为100ns);支持内存复用,减少数据拷贝次数,提升内存使用效率。
  • 编译器协同优化:KUPL库与毕昇编译器3.1.0深度协同,毕昇编译器可识别KUPL的核心接口,对计算节点的代码进行自动向量优化、多核并行优化,将串行计算逻辑转换为适配鲲鹏aarch64架构的并行指令,进一步提升执行性能。

三、实验步骤

3.1 HPCKit环境部署

HPCKit是本次实验的核心开发套件,包含KUPL库、毕昇编译器等依赖组件,部署过程需严格遵循鲲鹏架构的适配要求,具体步骤如下(所有操作均在root用户下执行):

  • 环境准备:检查服务器架构是否为aarch64,执行命令:uname -m,输出为“aarch64”即为正常;安装依赖工具,执行命令:dnf install -y wget tar gcc make,确保wget(下载工具)、tar(解压工具)可正常使用;关闭防火墙(避免影响软件下载和环境配置),执行命令:systemctl stop firewalld && systemctl disable firewalld
  • 下载HPCKit安装包:从鲲鹏社区官方网站下载鲲鹏HPCKit 2.3.0版本(适配aarch64架构),执行命令:wget https://www.hikunpeng.com/developer/hpc/hpckit-download -O HPCKit_2.3.0_Linux-aarch64.tar.gz;若下载速度较慢,可通过鲲鹏社区镜像源下载,替换命令中的下载地址即可;下载完成后,检查安装包完整性,执行命令:md5sum HPCKit_2.3.0_Linux-aarch64.tar.gz,对比鲲鹏社区提供的MD5校验值,确保安装包未损坏。
  • 解压并一键安装:创建安装目录,执行命令:mkdir -p /opt/hikunpeng/hpckit;解压安装包至临时目录,执行命令:tar -zxvf HPCKit_2.3.0_Linux-aarch64.tar.gz -C /tmp;进入解压目录,执行一键安装脚本,执行命令:cd /tmp/HPCKit_2.3.0_Linux-aarch64 && sh install.sh --prefix /opt/hikunpeng/hpckit;安装过程中,脚本会自动安装毕昇编译器3.1.0、KUPL库、KUPL依赖的pthread库等组件,无需手动干预,安装完成后,会提示“Installation completed successfully”。
  • 加载环境变量:安装完成后,需加载HPCKit的环境变量,让系统识别毕昇编译器、KUPL库等组件;执行临时加载命令(仅当前终端有效):source /opt/hikunpeng/hpckit/latest/setvars.sh;若需永久加载环境变量,编辑/etc/profile文件,执行命令:echo "source /opt/hikunpeng/hpckit/latest/setvars.sh" >> /etc/profile,执行命令:source /etc/profile使配置生效。
  • 环境验证:验证毕昇编译器版本,执行命令:bishengcc -v,输出“gcc version 3.1.0 (Bisheng Compiler 3.1.0)”即为正常;验证KUPL库是否安装成功,执行命令:ldconfig -p | grep libkupl,若输出libkupl.so相关信息,说明KUPL库安装成功;验证HPCKit版本,执行命令:hpckit --version,输出“2.3.0”即为正常。
  • 常见问题解决:① 若安装过程中提示“缺少依赖包”,执行命令:dnf install -y libstdc++-devel,安装缺失的依赖包后重新安装;② 若加载环境变量后,bishengcc命令无法识别,检查环境变量路径是否正确,重新执行source命令;③ 若KUPL库无法识别,检查安装目录下的lib文件夹是否存在libkupl.so文件,若不存在,重新解压安装包安装。

3.2 计算图代码编写

本次实验需编写基于KUPL计算图的代码,实现E = A*B + C*D的计算任务,结合鲲鹏Gitee开源的KUPL-Sample示例代码,具体步骤如下:

  • 获取KUPL-Sample示例代码:KUPL-Sample包含多种KUPL并行编程示例(计算图、多线程、内存优化等),可作为代码编写的参考;执行命令:git clone https://gitee.com/openeuler/kupl-sample.git,克隆代码至本地目录;进入示例代码目录,查看计算图相关示例,执行命令:cd kupl-sample && ls graph_sample,参考graph_demo.c文件的编写规范。
  • 创建代码文件:在服务器上创建代码目录,执行命令:mkdir -p /root/kupl_experiment && cd /root/kupl_experiment;创建代码文件kupl_graph_demo.c,执行命令:vim kupl_graph_demo.c(或通过VSCode远程编辑该文件)。
  • 编写完整代码:代码需包含头文件引入、数据结构定义、计算函数定义、静态图/动态图操作、主函数等部分,完整代码如下(包含详细注释,补充原代码缺失的头文件、数据结构、错误处理等内容):
#include <stdio.h>
#include <stdlib.h>
#include <kupl/kupl_graph.h>  // KUPL计算图核心头文件
#include <pthread.h>         // 线程库头文件

// 定义数据结构:存储乘法计算的输入输出数据(适配E = A*B + C*D的计算逻辑)
typedef struct {
    float* A_data;  // 输入数据A
    float* B_data;  // 输入数据B
    float* F_data;  // 输出数据F(F = A*B 或 F = C*D)
} data_axb_t;

// 定义数据结构:存储加法计算的输入输出数据
typedef struct {
    float* F1_data; // 输入数据F1(A*B的结果)
    float* F2_data; // 输入数据F2(C*D的结果)
    float* E_data;  // 输出数据E(E = F1 + F2)
} data_add_t;

// 乘法计算函数(绑定到计算节点1、节点2)
static void fun_axb(void *args) {
    if (args == NULL) {  // 错误处理:避免空指针异常
        printf("Error: fun_axb args is NULL\n");
        return;
    }
    data_axb_t *data = (data_axb_t *)args;
    // 执行乘法计算:F = A * B
    (*data->F_data) = (*data->A_data) * (*data->B_data);
    printf("Multiplication task completed: %.2f * %.2f = %.2f\n", 
           *data->A_data, *data->B_data, *data->F_data);
}

// 加法计算函数(绑定到计算节点3)
static void fun_add(void *args) {
    if (args == NULL) {  // 错误处理:避免空指针异常
        printf("Error: fun_add args is NULL\n");
        return;
    }
    data_add_t *data = (data_add_t *)args;
    // 执行加法计算:E = F1 + F2
    (*data->E_data) = (*data->F1_data) + (*data->F2_data);
    printf("Addition task completed: %.2f + %.2f = %.2f\n", 
           *data->F1_data, *data->F2_data, *data->E_data);
}

// 结果输出函数(绑定到计算节点4)
static void fun_output(void *args) {
    if (args == NULL) {
        printf("Error: fun_output args is NULL\n");
        return;
    }
    float* E_data = (float*)args;
    // 输出最终计算结果
    printf("Final calculation result: E = %.2f\n", *E_data);
}

int main() {
    // 1. 初始化输入输出数据(模拟实际计算数据)
    float A = 3.5f, B = 2.0f, C = 4.5f, D = 3.0f;  // 输入数据
    float F1 = 0.0f, F2 = 0.0f, E = 0.0f;          // 中间结果和最终结果

    // 2. 初始化数据结构,绑定输入输出数据
    data_axb_t data1 = {&A, &B, &F1};  // 节点1的数据(计算A*B=F1)
    data_axb_t data2 = {&C, &D, &F2};  // 节点2的数据(计算C*D=F2)
    data_add_t data3 = {&F1, &F2, &E}; // 节点3的数据(计算F1+F2=E)

    // 3. 创建静态图(DAG),指定静态图配置(默认配置,适配鲲鹏架构)
    kupl_sgraph_t* sgraph = kupl_sgraph_create(NULL);
    if (sgraph == NULL) {  // 错误处理:静态图创建失败
        printf("Error: Failed to create static graph\n");
        return -1;
    }

    // 4. 配置节点描述符,添加4个计算节点到静态图
    // 节点1:执行A*B乘法计算
    kupl_sgraph_node_desc_t node1_desc = {
        .func = fun_axb,    // 绑定乘法计算函数
        .args = &data1,     // 绑定节点数据
        .name = "node1_axb" // 节点名称(用于调试)
    };
    kupl_sgraph_node_t* node1 = kupl_sgraph_add_node(sgraph, &node1_desc);
    if (node1 == NULL) {
        printf("Error: Failed to add node1\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 节点2:执行C*D乘法计算
    kupl_sgraph_node_desc_t node2_desc = {
        .func = fun_axb,
        .args = &data2,
        .name = "node2_axb"
    };
    kupl_sgraph_node_t* node2 = kupl_sgraph_add_node(sgraph, &node2_desc);
    if (node2 == NULL) {
        printf("Error: Failed to add node2\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 节点3:执行F1+F2加法计算
    kupl_sgraph_node_desc_t node3_desc = {
        .func = fun_add,
        .args = &data3,
        .name = "node3_add"
    };
    kupl_sgraph_node_t* node3 = kupl_sgraph_add_node(sgraph, &node3_desc);
    if (node3 == NULL) {
        printf("Error: Failed to add node3\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 节点4:输出最终结果E
    kupl_sgraph_node_desc_t node4_desc = {
        .func = fun_output,
        .args = &E,
        .name = "node4_output"
    };
    kupl_sgraph_node_t* node4 = kupl_sgraph_add_node(sgraph, &node4_desc);
    if (node4 == NULL) {
        printf("Error: Failed to add node4\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 5. 定义节点间的依赖关系,构建DAG有向无环图
    // 依赖1:node1执行完成后,才能执行node3(F1是node3的输入)
    if (kupl_sgraph_add_dep(node1, node3) != KUPL_SUCCESS) {
        printf("Error: Failed to add dependency node1 -> node3\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }
    // 依赖2:node2执行完成后,才能执行node3(F2是node3的输入)
    if (kupl_sgraph_add_dep(node2, node3) != KUPL_SUCCESS) {
        printf("Error: Failed to add dependency node2 -> node3\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }
    // 依赖3:node3执行完成后,才能执行node4(E是node4的输入)
    if (kupl_sgraph_add_dep(node3, node4) != KUPL_SUCCESS) {
        printf("Error: Failed to add dependency node3 -> node4\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 6. 创建动态图,启用鲲鹏服务器所有CPU核心(KUPL_ALL_EXECUTORS)
    kupl_graph_t* graph = kupl_graph_create(KUPL_ALL_EXECUTORS, NULL);
    if (graph == NULL) {
        printf("Error: Failed to create dynamic graph\n");
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 7. 将静态图封装为任务,提交至动态图执行
    kupl_sgraph_desc_t sgraph_desc = {.sgraph = sgraph};
    kupl_task_info_t task_info = {
        .type = KUPL_TASK_TYPE_SGRAPH,  // 任务类型:静态图任务
        .desc = &sgraph_desc            // 绑定静态图描述符
    };
    if (kupl_graph_submit(graph, &task_info) != KUPL_SUCCESS) {
        printf("Error: Failed to submit task to dynamic graph\n");
        kupl_graph_destroy(graph);
        kupl_sgraph_destroy(sgraph);
        return -1;
    }

    // 8. 阻塞等待所有任务执行完成(等待node4输出结果后,再继续执行)
    printf("Waiting for all tasks to complete...\n");
    kupl_graph_wait(graph);

    // 9. 销毁静态图、动态图,释放资源(避免内存泄漏)
    kupl_graph_destroy(graph);
    kupl_sgraph_destroy(sgraph);

    // 10. 验证计算结果(手动计算:E = 3.5*2.0 + 4.5*3.0 = 7.0 + 13.5 = 20.5)
    float expected_E = A*B + C*D;
    if (abs(E - expected_E) < 1e-6) {  // 浮点数比较,允许微小误差
        printf("Experiment succeeded: Calculation result is correct!\n");
    } else {
        printf("Experiment failed: Calculation result is incorrect!\n");
        printf("Expected E = %.2f, Actual E = %.2f\n", expected_E, E);
        return -1;
    }

    return 0;
}

代码补充说明:① 增加了错误处理逻辑(如静态图/动态图创建失败、节点添加失败的判断),提升代码健壮性;② 补充了4个计算节点(相比原代码增加了结果输出节点),使计算流程更完整;③ 增加了数据结构的详细定义,明确输入输出数据的绑定关系;④ 增加了打印信息,便于查看任务执行过程和结果;⑤ 增加了手动计算结果验证,自动判断实验是否成功。

3.3 毕昇编译器编译

使用毕昇编译器编译代码,链接KUPL库和pthread线程库,开启编译优化,适配鲲鹏aarch64架构,具体步骤如下:

  • 进入代码目录,执行命令:cd /root/kupl_experiment
  • 执行编译命令,结合毕昇编译器的优化选项,编译命令如下: bishengcc -o kupl_graph_demo kupl_graph_demo.c -lkupl -lpthread -O2 -march=armv8.2-a -mtune=kunpeng920
  • 编译选项详细说明(补充知识点):
    • -o kupl_graph_demo:指定编译后的可执行文件名称为kupl_graph_demo。
    • -lkupl:链接KUPL库(指定程序运行时依赖libkupl.so库)。
    • -lpthread:链接pthread线程库(KUPL库底层依赖该库实现多线程)。
    • -O2:开启二级编译优化,是毕昇编译器针对鲲鹏架构的推荐优化级别,可实现自动向量优化、循环展开、函数内联等优化操作,提升程序运行性能,同时避免过高优化(如-O3)导致的代码调试困难。
    • -march=armv8.2-a:指定目标架构为armv8.2-a(鲲鹏920处理器的架构版本),确保编译生成的指令适配鲲鹏aarch64架构。
    • -mtune=kunpeng920:针对鲲鹏920处理器进行指令优化,让编译器生成更适配鲲鹏920核心架构的指令,进一步提升执行效率。
  • 编译结果验证:编译成功后,代码目录下会生成kupl_graph_demo可执行文件,执行命令:ls -l kupl_graph_demo,若输出可执行文件信息(权限为rwxr-xr-x),即为编译成功。
  • 常见编译错误解决:① 若提示“fatal error: kupl/kupl_graph.h: No such file or directory”,说明KUPL库的头文件未找到,检查环境变量是否加载成功,重新执行source命令;② 若提示“undefined reference to `kupl_sgraph_create'”,说明未链接KUPL库,检查编译命令中是否包含-lkupl选项;③ 若提示“architecture mismatch”,说明编译选项中的-march参数错误,确保参数为armv8.2-a。

3.4 程序运行与结果验证

运行编译后的可执行文件,查看任务执行过程和计算结果,验证KUPL计算图的并行执行效果,同时结合鲲鹏服务器的性能监控工具,查看多核使用情况,具体步骤如下:

  • 程序运行:在代码目录下,执行命令:./kupl_graph_demo;运行过程中,程序会打印每个任务的执行信息(乘法、加法任务的计算结果)和最终结果,同时提示实验是否成功。
  • 预期运行结果(补充完整输出):Waiting for all tasks to complete... Multiplication task completed: 3.50 * 2.00 = 7.00 Multiplication task completed: 4.50 * 3.00 = 13.50 Addition task completed: 7.00 + 13.50 = 20.50 Final calculation result: E = 20.50 Experiment succeeded: Calculation result is correct!说明:两个乘法任务(node1、node2)并行执行,执行顺序可能颠倒(取决于动态图的调度),但最终结果一致,均为E=20.50,与手动计算结果一致,说明计算图任务执行成功。
  • 结果验证(补充两种验证方式):
    • 手动验证:手动计算E = A*B + C*D = 3.5×2.0 + 4.5×3.0 = 7.0 + 13.5 = 20.5,与程序输出结果一致,说明计算逻辑正确。
    • 日志验证:查看程序运行日志(若需保存日志,执行命令:./kupl_graph_demo &gt; run.log 2&gt;&amp;1),检查日志中是否有错误提示,是否所有任务均执行完成,最终结果是否正确。
  • 性能监控(补充知识点:鲲鹏多核并行效果验证):
    • 开启另一个终端,远程连接鲲鹏服务器,执行命令:htop,查看CPU核心的使用情况;运行程序时,可观察到多个CPU核心的使用率升高(节点1、node2并行执行,会占用2个核心,后续节点3、node4依次执行,核心使用率动态变化),说明KUPL计算图成功实现了多线程并行执行,充分利用了鲲鹏多核算力。
    • 统计程序运行时间,执行命令:time ./kupl_graph_demo,查看程序的实际运行时间(本次实验中,程序运行时间约为0.001-0.002秒,由于计算任务简单,主要耗时在静态图/动态图的初始化和资源释放上;若为复杂计算任务,并行执行的时间优势会更明显)。
  • 异常情况处理:① 若程序运行提示“segmentation fault (core dumped)”,说明出现内存错误,检查代码中的空指针、数组越界等问题,可通过gdb调试(执行命令:gdb ./kupl_graph_demo),定位错误位置;② 若程序运行结果不正确,检查计算函数的逻辑、节点依赖关系的定义是否正确;③ 若程序运行缓慢,检查CPU核心是否被其他进程占用,关闭无关进程后重新运行。

四、关键代码解析

4.1 静态图创建与任务节点定义

静态图是KUPL计算图的核心,负责定义任务和数据依赖,核心代码围绕静态图创建、节点描述符配置、节点添加展开,结合鲲鹏架构的适配特点,详细解析如下:

#include <kupl/kupl_graph.h>  // 必须引入KUPL计算图头文件,否则无法调用核心接口

// 1. 数据结构定义(核心:绑定输入输出数据,适配计算逻辑)
typedef struct {
    float* A_data;  // 输入数据指针,指向外部定义的变量A
    float* B_data;  // 输入数据指针,指向外部定义的变量B
    float* F_data;  // 输出数据指针,指向中间结果F
} data_axb_t;

// 2. 计算函数定义(核心:封装计算逻辑,将函数绑定到节点)
static void fun_axb(void *args) {
    if (args == NULL) {  // 错误处理:鲲鹏平台对内存访问要求严格,避免空指针异常
        printf("Error: fun_axb args is NULL\n");
        return;
    }
    data_axb_t *data = (data_axb_t *)args;  // 类型转换,获取节点绑定的数据
    (*data->F_data) = (*data->A_data) * (*data->B_data);  // 核心计算逻辑
}

// 3. 静态图创建(核心接口:kupl_sgraph_create)
kupl_sgraph_t* sgraph = kupl_sgraph_create(NULL);
// 解析:
// - 函数原型:kupl_sgraph_t* kupl_sgraph_create(const kupl_sgraph_attr_t* attr);
// - 参数attr:静态图配置属性,传入NULL表示使用默认配置(适配鲲鹏架构,节点最大数量、缓存大小自动调整);
// - 返回值:静态图实例指针,若为NULL,说明静态图创建失败(如内存不足);
// - 鲲鹏适配:该接口底层会申请鲲鹏服务器的内存资源,优先使用L3缓存,提升数据访问效率。

// 4. 节点描述符配置(核心:绑定函数、数据、节点名称)
kupl_sgraph_node_desc_t node1_desc = {
    .func = fun_axb,    // 绑定计算函数(fun_axb为乘法计算逻辑)
    .args = &data1,     // 绑定数据结构实例(data1存储A、B、F1的指针)
    .name = "node1_axb" // 节点名称,用于调试(可通过KUPL工具查看节点执行状态)
};
// 解析:节点描述符是连接计算逻辑和静态图的桥梁,KUPL通过该描述符,将计算函数和数据绑定到节点,
// 动态图调度时,会自动调用节点绑定的函数,并传入绑定的数据,实现任务的自动化执行。

// 5. 添加节点到静态图(核心接口:kupl_sgraph_add_node)
kupl_sgraph_node_t* node1 = kupl_sgraph_add_node(sgraph, &node1_desc);
// 解析:
// - 函数原型:kupl_sgraph_node_t* kupl_sgraph_add_node(kupl_sgraph_t* sgraph, const kupl_sgraph_node_desc_t* desc);
// - 参数sgraph:静态图实例指针,指定节点要添加到的静态图;
// - 参数desc:节点描述符指针,指定节点的配置信息;
// - 返回值:节点实例指针,若为NULL,说明节点添加失败(如静态图实例无效);
// - 鲲鹏适配:添加节点时,KUPL会自动为节点分配鲲鹏服务器的内存资源,优化数据存储位置,适配多级内存架构。

补充知识点:静态图的节点类型除了计算节点,还可添加数据节点、控制节点,本次实验仅使用计算节点,若需处理复杂数据移动,可添加数据节点(通过kupl_sgraph_add_data_node接口),实现数据在不同节点间的高效传递,适配鲲鹏服务器的内存带宽特性。

4.2 节点依赖关系添加

节点依赖关系是构建DAG有向无环图的核心,直接决定任务的执行顺序,避免数据竞争(如节点3未获取节点1、节点2的结果就执行加法运算),核心代码解析如下:

// 核心接口:kupl_sgraph_add_dep(添加节点间的依赖关系)
kupl_sgraph_add_dep(node1, node3);  // node1执行完成后,才能执行node3
kupl_sgraph_add_dep(node2, node3);  // node2执行完成后,才能执行node3
kupl_sgraph_add_dep(node3, node4);  // node3执行完成后,才能执行node4

// 解析:
// 1. 函数原型:kupl_status_t kupl_sgraph_add_dep(kupl_sgraph_node_t* prev_node, kupl_sgraph_node_t* next_node);
// 2. 参数说明:
//    - prev_node:前置节点(依赖节点),即必须先执行完成的节点(如node1、node2);
//    - next_node:后置节点(被依赖节点),即需要等待前置节点执行完成才能执行的节点(如node3、node4);
// 3. 返回值:KUPL_SUCCESS表示依赖关系添加成功,其他值表示失败(如节点无效、存在循环依赖);
// 4. 依赖关系逻辑(对应E = A*B + C*D):
//    - node1(A*B)和node2(C*D)无前置依赖,可并行执行(动态图会分配两个线程,同时执行这两个节点);
//    - node3(F1+F2)的前置依赖是node1和node2,必须等待两个乘法任务都完成,才能执行加法运算;
//    - node4(输出E)的前置依赖是node3,必须等待加法任务完成,才能输出最终结果;
// 5. 鲲鹏适配:该接口底层会构建依赖关系表,动态图调度时,会根据鲲鹏CPU核心的负载情况,
//    调度前置节点并行执行,充分利用多核算力,同时确保数据的正确性,避免数据竞争。

// 补充:依赖关系校验(避免循环依赖)
// 若添加如下依赖,会返回失败(node3依赖node4,node4依赖node3,形成循环):
// kupl_sgraph_add_dep(node3, node4);
// kupl_sgraph_add_dep(node4, node3);
// 循环依赖会导致DAG图无效,静态图无法提交至动态图执行,KUPL会自动检测并返回错误。

补充知识点:KUPL支持批量添加依赖关系(通过kupl_sgraph_add_deps接口),适合节点数量较多的场景,可简化代码编写;同时支持删除依赖关系(通过kupl_sgraph_remove_dep接口),提升静态图的灵活性,适配动态调整任务流程的场景。

4.3 动态图提交与资源释放

动态图负责调度静态图任务的并行执行,核心代码围绕动态图创建、任务提交、阻塞等待、资源释放展开,结合鲲鹏多核架构的调度特点,详细解析如下:

#include <kupl/kupl_graph.h>

// 1. 创建动态图(核心接口:kupl_graph_create)
kupl_graph_t* graph = kupl_graph_create(KUPL_ALL_EXECUTORS, NULL);
// 解析:
// - 函数原型:kupl_graph_t* kupl_graph_create(kupl_executor_mask_t executor_mask, const kupl_graph_attr_t* attr);
// - 参数executor_mask:执行器掩码,指定动态图可使用的CPU核心;
//   - KUPL_ALL_EXECUTORS:启用鲲鹏服务器的所有CPU核心(本次实验为96核),最大化利用多核并行算力;
//   - 也可指定特定核心(如KUPL_EXECUTOR_MASK(0,1),仅使用第0、1核心);
// - 参数attr:动态图配置属性,传入NULL表示使用默认配置(负载均衡调度策略);
// - 返回值:动态图实例指针,若为NULL,说明动态图创建失败;
// - 鲲鹏适配:动态图创建时,会自动识别鲲鹏920服务器的核心数量,初始化线程池,线程数量与核心数量匹配,
//   避免线程过多导致的上下文切换开销,适配鲲鹏多核架构的调度需求。

// 2. 封装静态图为任务,提交至动态图(核心接口:kupl_graph_submit)
kupl_sgraph_desc_t sgraph_desc = {.sgraph = sgraph};  // 静态图描述符,绑定静态图实例
kupl_task_info_t task_info = {
    .type = KUPL_TASK_TYPE_SGRAPH,  // 任务类型:静态图任务(KUPL支持多种任务类型)
    .desc = &sgraph_desc            // 任务描述符,绑定静态图描述符
};
kupl_graph_submit(graph, &task_info);  // 提交任务至动态图
// 解析:
// - 函数原型:kupl_status_t kupl_graph_submit(kupl_graph_t* graph, const kupl_task_info_t* task_info);
// - 参数graph:动态图实例指针;
// - 参数task_info:任务信息,指定任务类型和任务描述符;
// - 返回值:KUPL_SUCCESS表示任务提交成功;
// - 提交后,动态图会立即解析静态图的DAG结构,调度节点并行执行,无需手动干预,
//   动态图会根据核心负载情况,分配线程执行节点任务(如node1、node2分配两个独立线程)。

// 3. 阻塞等待任务执行完成(核心接口:kupl_graph_wait)
kupl_graph_wait(graph);
// 解析:
// - 函数原型:kupl_status_t kupl_graph_wait(kupl_graph_t* graph);
// - 作用:阻塞当前主线程,等待动态图中所有任务(静态图的所有节点)执行完成,
//   确保主线程不会提前退出,导致任务未执行完成;
// - 鲲鹏适配:该接口底层会等待所有线程池中的线程执行完成,获取每个节点的执行状态,
//   确保任务执行的完整性,适配高性能计算中对任务执行可靠性的要求。

// 4. 资源释放(核心接口:kupl_graph_destroy、kupl_sgraph_destroy)
kupl_graph_destroy(graph);    // 销毁动态图,释放线程池、动态图占用的内存资源
kupl_sgraph_destroy(sgraph);  // 销毁静态图,释放节点、依赖关系占用的内存资源
// 解析:
// - 函数原型:void kupl_graph_destroy(kupl_graph_t* graph);
//              void kupl_sgraph_destroy(kupl_sgraph_t* sgraph);
// - 作用:手动销毁动态图和静态图实例,释放占用的内存、线程等资源,避免内存泄漏;
// - 注意事项:必须先销毁动态图,再销毁静态图(动态图依赖静态图实例),
//   否则会导致内存错误,这是鲲鹏高性能编程中资源管理的重要规范,
//   因为鲲鹏服务器的内存资源宝贵,内存泄漏会严重影响后续高性能计算任务的执行。

4.4 补充:性能优化代码解析(适配鲲鹏架构)

为充分发挥鲲鹏920服务器的多核算力和多级内存架构优势,可在代码中添加性能优化逻辑,核心优化代码及解析如下(可集成到上述实验代码中):

#include <kupl/kupl_mem.h>  // KUPL内存管理头文件(用于内存优化)

// 优化1:使用KUPL内存管理接口,分配缓存友好的内存(适配鲲鹏多级内存架构)
// 替换原有的普通内存分配(如float A = 3.5f),使用KUPL内存分配接口:
float* A = (float*)kupl_mem_alloc(sizeof(float), KUPL_MEM_CACHE_L3);  // 分配L3缓存内存
float* B = (float*)kupl_mem_alloc(sizeof(float), KUPL_MEM_CACHE_L3);
*A = 3.5f;
*B = 2.0f;
// 解析:
// - kupl_mem_alloc:KUPL内存分配接口,可指定内存存储位置(L3缓存、主存);
// - KUPL_MEM_CACHE_L3:将数据存储在L3缓存中,L3缓存访问延迟低,适合高频访问的数据;
// - 鲲鹏适配:鲲鹏920处理器的L3缓存容量大(每颗64MB),将高频访问的计算数据存储在L3缓存,
//   可大幅降低内存访问延迟,提升计算效率,尤其适合复杂迭代计算场景。

// 优化2:绑定线程到特定CPU核心(避免线程上下文切换)
// 在动态图创建后,添加线程绑定配置:
kupl_graph_attr_t graph_attr;
kupl_graph_attr_init(&graph_attr);  // 初始化动态图配置
graph_attr.thread_affinity = 1;     // 启用线程核心绑定
kupl_graph_t* graph = kupl_graph_create(KUPL_ALL_EXECUTORS, &graph_attr);
// 解析:
// - thread_affinity = 1:启用线程与CPU核心的绑定,动态图分配的线程会固定运行在特定核心上;
// - 鲲鹏适配:避免线程在不同核心间频繁切换,降低上下文切换开销,充分利用每个核心的算力,
//   尤其适合长时间运行的高性能计算任务,可提升任务执行的稳定性和效率。

// 优化3:开启编译优化(结合毕昇编译器的鲲鹏专属优化)
// 编译命令中添加更多优化选项:
// bishengcc -o kupl_graph_demo kupl_graph_demo.c -lkupl -lpthread -O2 -march=armv8.2
收藏举报
Level 1
0
帖子
0
粉丝
0
获赞