资讯动态

使用 MAX C API 执行预编译图:MEF 序列化、外部权重与设备图捕获实战

发布时间:2026/9/12 3:16:44 来源:尧图企业网站定制
使用 MAX C API 执行预编译图MEF 序列化、外部权重与设备图捕获实战【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojoMAX 图 APIMAX graph API为在 GPU、CPU 等设备上构建计算图提供了完整框架其编译与执行过程可以彻底分离。本文以 Modular 平台开源仓库中的 C API 示例集合为蓝本系统讲解用 MAX Python API 构建并编译图 → 序列化为 MEF 文件 → 用 C 程序加载并执行的完整链路并深入剖析外部权重注册、Safetensors 权重加载、设备图捕获与回放CUDA Graphs 思路等生产级能力帮助你掌握将 MAX 编译产物集成进 C/C 服务端程序的实战方案。[!NOTE] 本文涉及的 C 接口正处于快速演进阶段未来可能发生显著变化。此外MEF 文件格式并非通用的序列化方案它是设备相关的只能在两台相似系统之间迁移不能当作跨平台模型交换格式。示例总览一条 Python 到 C 的完整执行链路示例位于仓库的max/examples/capi/目录围绕同一主题组织为五组可独立运行的配套脚本与 C 程序示例Python 构建脚本C 执行程序核心主题基础向量加法test_capi.pyexample.cPython 构建图 → MEF → C 执行新版 API 捕获test_capi_v3.py复用example.cmax.experimental.nn.Moduleexport_mef()外部权重test_weights_capi.pyweights_example.c运行时权重注册表Safetensors 权重test_safetensors_capi.pysafetensors_example.c从文件批量加载权重设备图捕获test_graph_capture.pygraph_capture.cCUDA/HIP 图捕获与回放每个 Python 脚本的职责一致构建图 → 编译 → 导出 MEF 文件 → 通过环境变量指定的路径execv启动 C 程序。C 程序则负责加载 MEF、初始化运行时、执行推理并校验结果。一键运行Pixi 任务Pixi 是 Modular 官方推荐的依赖与任务管理工具。在max/examples/capi/pixi.toml中定义了完整的构建与测试任务[tasks] build cc example.c -I $CONDA_PREFIX/include -L$CONDA_PREFIX/lib -Wl,-rpath,$CONDA_PREFIX/lib -lmax -o graph_executor build-weights cc weights_example.c -I $CONDA_PREFIX/include -L$CONDA_PREFIX/lib -Wl,-rpath,$CONDA_PREFIX/lib -lmax -o weights_executor [tasks.test] depends-on [build] env { GRAPH_EXECUTOR graph_executor } cmd python test_capi.py [tasks.test-v3] depends-on [build] env { GRAPH_EXECUTOR graph_executor } cmd python test_capi_v3.py [tasks.test-weights] depends-on [build-weights] env { WEIGHTS_EXECUTOR weights_executor } cmd python test_weights_capi.py运行整个流程构建图、编译、保存到磁盘、在 GPU 上执行只需一条命令pixi run test这条命令会先执行build任务用cc将example.c编译为graph_executor链接-lmax库然后通过GRAPH_EXECUTOR环境变量把可执行文件路径传给 Python 脚本。仓库同时提供了 Bazel 构建方式见 BUILD.bazel其中 GPU 相关的 target 带有target_compatible_with [//:has_gpu]约束。基础流程构建向量加法图并导出 MEF第一步用 Python 构建与编译图test_capi.py构建了一个对两个向量做逐元素加法的图关键在于使用符号维度vector_width声明输入形状使图能够接收任意等长的向量对from max import engine from max.driver import Accelerator from max.dtype import DType from max.graph import DeviceRef, Graph, TensorType def build_graph() - None: # 为加速器构建图 device Accelerator() # 输入张量预期位于加速器上。vector_width 是符号维度 # 允许输入向量使用动态形状。 input_type TensorType( dtypeDType.float32, shape(vector_width,), deviceDeviceRef.from_device(device), ) # 图仅包含一步操作向量加法 with Graph(vector_add, input_types(input_type, input_type)) as graph: vector1, vector2 graph.inputs[0].tensor, graph.inputs[1].tensor output vector1 vector2 # 等价于 ops.add() graph.output(output) # 为目标设备编译图 session engine.InferenceSession(devices[device]) compiled session.compile(graph) model session.init(compiled) # 将图保存为 MEF 文件 model._export_mef(graph.mef)关键点解读图运行在 GPU 上输入和输出张量均被指定驻留在加速器DeviceRef.from_device(device)_export_mef(graph.mef)将编译产物序列化到磁盘形成 C 端消费的 MEF 文件图输入自动命名为input0/input1输出命名为output0这是 C 端按名称取张量的契约基础。第二步用 C 程序加载并执行example.c完整演示了 C API 的执行流水线其核心步骤如下1. 初始化运行时与设备M_Status *status M_newStatus(); M_RuntimeConfig *runtimeConfig M_newRuntimeConfig(); // 校验加速器可用 int acceleratorCount M_getAcceleratorCount(); if (acceleratorCount 0) { printf(Error: No accelerator detected. This example requires a GPU.\n); goto cleanupRuntimeConfig; } // 创建 host 设备用于从 CPU 内存借用数据 M_Device *host M_newDevice(M_HOST, 0, status); M_runtimeConfigAddDevice(runtimeConfig, host); // 创建加速器设备 M_Device *accelerator M_newDevice(M_ACCELERATOR, 0, status); M_runtimeConfigAddDevice(runtimeConfig, accelerator); M_RuntimeContext *context M_newRuntimeContext(runtimeConfig, status);2. 从 MEF 加载并初始化模型M_CompileConfig *compileConfig M_newCompileConfig(); M_setModelPath(compileConfig, graph.mef); M_AsyncCompiledModel *compiledModel M_compileModelSync(context, compileConfig, status); M_AsyncModel *model M_initModel(context, compiledModel, NULL, status);M_compileModelSync负责加载 MEFM_initModel的第三个参数是权重注册表指针此处为NULL外部权重示例会用到。3. 构造输入张量并搬运到加速器float vector1[8] {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; float vector2[8] {8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f}; int64_t shape[1] {8}; // 用 host 上的张量规格借用数据零拷贝 M_TensorSpec *hostInputSpec1 M_newTensorSpec(shape, 1, M_FLOAT32, input0, host); M_AsyncTensorMap *hostInputs M_newAsyncTensorMap(context); M_borrowTensorInto(hostInputs, vector1, hostInputSpec1, status); // 取出张量并复制到加速器 M_AsyncTensor *hostInputTensor1 M_getTensorByNameFrom(hostInputs, input0, status); M_AsyncTensor *acceleratorInputTensor1 M_copyTensorToDevice(hostInputTensor1, accelerator, status);这里演示了数据搬运的标准模式先用M_borrowTensorInto借用 host 内存避免复制再通过M_copyTensorToDevice显式迁移到设备端。随后以加速器上的张量数据为源构建新的输入张量映射M_TensorSpec *acceleratorInputSpec1 M_newTensorSpec(shape, 1, M_FLOAT32, input0, accelerator); const void *acceleratorData1 M_getTensorData(acceleratorInputTensor1); M_borrowTensorInto(inputs, (void *)(uintptr_t)acceleratorData1, acceleratorInputSpec1, status);4. 执行推理并校验结果M_AsyncTensorMap *outputs M_executeModelSync(context, model, inputs, status); M_AsyncTensor *outputTensor M_getTensorByNameFrom(outputs, output0, status); // 输出复制回 host 读取 M_AsyncTensor *hostOutputTensor M_copyTensorToDevice(outputTensor, host, status); const float *outputData (const float *)M_getTensorData(hostOutputTensor);示例对输出做了逐元素校验两个输入向量逐位相加恰好都是9.0因此期望输出为[9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0]全部匹配即打印Vector addition successful!。5. 按分配逆序释放资源example.c结尾用goto标签链cleanupHostOutputTensor → cleanupOutputTensor → … → cleanupRuntimeConfig实现了与分配顺序严格相反的确定性释放每个M_对象都有对应的M_free*释放函数这是使用 C API 时避免资源泄漏的规范写法。新 API 路线用 max.experimental.nn.Module 捕获 MEFtest_capi_v3.py是test_capi.py的新版 API 对应实现。它把同一个向量加法图构建为一个max.experimental.nn.Module通过公开方法CompiledModel.export_mef()导出 MEFfrom max.driver import Accelerator from max.dtype import DType from max.experimental.nn import Module, module_dataclass from max.experimental.tensor import Tensor from max.graph import DeviceRef, TensorType module_dataclass class VectorAdd(Module[[Tensor, Tensor], Tensor]): 对两个向量逐元素相加。 def forward(self, vector1: Tensor, vector2: Tensor) - Tensor: return vector1 vector2 def build_graph() - None: device Accelerator() model VectorAdd().to(device) input_type TensorType( dtypeDType.float32, shape(vector_width,), deviceDeviceRef.from_device(device), ) # 编译模块输入类型必须与 forward 的位置参数一致 compiled model.compile(input_type, input_type) # 从编译产物直接序列化 MEF 文件 compiled.export_mef(graph.mef)两条 API 路线产出的graph.mef都能被同一个example.c消费因为两者对图输入/输出的命名约定完全一致输入为input0/input1输出为output0因此 C 代码无需任何改动。export_mef()与旧式_export_mef的关键差异在于它直接从编译产物序列化不需要模型在真实设备上初始化。这意味着它适用于交叉编译和虚拟设备场景——这正是生产环境通过 MAX C API 做服务化推理所依赖的能力。另一个值得注意的细节是新版 API 在加速器上默认使用 bfloat16而example.c按 float32 读取并校验输出因此构建脚本必须显式指定DType.float32避免类型不匹配导致结果校验失败。外部权重运行时通过权重注册表注入模型参数当模型的权重与编译图分开存储例如存放于 safetensors 或 GGUF 文件时就需要在运行时通过 C API 的权重注册表提供权重。构建引用外部权重的图test_weights_capi.py用ops.constant_external()在图中声明一个名为weight的外部权重占位图的语义为output input * weightimport numpy as np from max import engine from max.driver import CPU from max.dtype import DType from max.graph import DeviceRef, Graph, TensorType, ops def build_graph() - None: input_type TensorType( dtypeDType.float32, shape(4,), deviceDeviceRef.CPU() ) weight_type TensorType( dtypeDType.float32, shape(4,), deviceDeviceRef.CPU() ) with Graph(weighted_multiply, input_types(input_type,)) as graph: inp graph.inputs[0].tensor weight ops.constant_external(weight, weight_type) graph.output(inp * weight) # 编译期使用哑权重占位——真实数值在运行时由 C 程序提供 dummy_weights {weight: np.zeros(4, dtypenp.float32)} session engine.InferenceSession(devices[CPU()]) compiled session.compile(graph) model session.init(compiled, weights_registrydummy_weights) model._export_mef(weights_graph.mef)注意编译期只需形状与 dtype 匹配的哑权重np.zeros真实权重数值完全由 C 端在运行时注入。该示例在CPU上运行不要求 GPU。C 端创建权重注册表weights_example.c演示了核心的注册表操作#include max/c/weights.h // 图期望一个名为 weight、形状 (4,) 的 float32 权重。 // 真实场景下这些数值通常来自文件如 safetensors。 float weightData[4] {2.0f, 3.0f, 4.0f, 5.0f}; const char *weightNames[1] {weight}; const void *weightPtrs[1] {weightData}; M_WeightsRegistry *weights M_newWeightsRegistry(weightNames, weightPtrs, 1, status); // 初始化模型时把注册表交给运行时 M_AsyncModel *model M_initModel(context, compiledModel, weights, status);M_newWeightsRegistry接收三个平行数组权重名称、权重数据指针和权重数量。之后执行M_executeModelSync时运行时自动把注册表中的权重与图中constant_external(weight, ...)占位符对应起来。输入[1, 2, 3, 4]乘以权重[2, 3, 4, 5]期望输出[2, 6, 12, 20]。Safetensors 加载从文件一步构建权重注册表safetensors_example.c在权重注册表基础上更进一步权重数据直接来自 Safetensors 文件而非内存数组。它依赖独立的头文件max/c/safetensors.h。生成 Safetensors 文件test_safetensors_capi.py构建了与外部权重示例完全相同的constant_external()图并额外用标准库手工写出一个最小合法的weights.safetensors文件8 字节小端长度前缀 JSON 头 二进制数据块JSON 头按 8 字节边界填充def write_safetensors() - None: weight np.array([2.0, 3.0, 4.0, 5.0], dtypenp.float32) blob weight.tobytes() header { weight: { dtype: F32, shape: list(weight.shape), data_offsets: [0, len(blob)], } } header_bytes json.dumps(header).encode(utf-8) padding (-(8 len(header_bytes))) % 8 header_bytes b * padding with open(weights.safetensors, wb) as f: f.write(struct.pack(Q, len(header_bytes))) f.write(header_bytes) f.write(blob)C 端加载与注册safetensors_example.c展示了三步式加载流程#include max/c/safetensors.h // 1. 将文件加载到 host 设备运行时在模型初始化时负责拷到模型设备 const char *paths[1] {weights.safetensors}; M_Safetensors *safetensors M_loadSafetensors(paths, 1, host, status); // 2. 检视文件包含的张量 printf(Loaded %zu tensor(s) from Safetensors file:\n, M_getSafetensorCount(safetensors)); for (size_t i 0; i M_getSafetensorCount(safetensors); i) { const char *name M_getSafetensorName(safetensors, i); size_t numBytes M_getSafetensorNumBytes(safetensors, name, status); printf( - %s (%zu bytes)\n, name, numBytes); } // 3. 一步从文件中所有张量构建权重注册表 M_WeightsRegistry *weights M_newWeightsRegistryFromSafetensors(safetensors, status); // 4. 与外部权重示例一致地初始化模型 M_AsyncModel *model M_initModel(context, compiledModel, weights, status);M_newWeightsRegistryFromSafetensors一步完成文件中所有张量 → 权重注册表的转换省去逐张量手工注册。需要特别注意的是命名契约文件中的张量名必须与图中的外部权重名完全一致——本示例不做任何名称翻译。同时权重先加载到 host 设备运行时在模型初始化阶段自动将其复制到模型所在的设备上。设备图捕获与回放降低重复推理的启动开销graph_capture.c演示了设备图捕获如 CUDA Graphs的用法把一次模型执行录制为可回放的设备图后续运行以接近零的启动开销重复执行非常适合同形状输入反复推理的延迟敏感场景。示例贯穿三个核心操作1. 捕获M_captureModelSyncuint64_t graphKey 1; M_AsyncTensor *inputs[2] {gpuInput1, gpuInput2}; size_t numOutputs 0; M_AsyncTensor **capturedOutputs M_captureModelSync( context, model, graphKey, 1, inputs, 2, numOutputs, status); if (M_isError(status)) { printf(Device graph capture is not supported on this accelerator.\n); printf(This feature requires a CUDA or HIP GPU.\n); goto cleanup_gpu_inputs; }M_captureModelSync先正常执行一次模型并录制整个执行过程返回的输出张量数组在每次回放时被就地更新。graphKeyuint64_t用于在后续回放中标识这张捕获图。2. 回放M_replayModelSyncM_replayModelSync(context, model, graphKey, 1, inputs, 2, status);以接近零的启动开销重放已捕获的图结果直接出现在捕获阶段返回的输出张量中。必须复用同一批输入张量相同的缓冲区地址因为捕获的图与具体的内存地址绑定。3. 调试校验M_debugVerifyReplayModelSyncM_debugVerifyReplayModelSync(context, model, graphKey, 1, inputs, 2, status);以 eager 方式重新执行模型并把内核启动轨迹与捕获图进行对比用于开发阶段验证捕获图始终正确。[!NOTE] 设备图捕获要求 CUDA 或 HIP GPU在 Apple GPUMetal或纯 CPU 系统上不可用。仓库 Bazel 构建中test_graph_capture同样带tags [gpu]与//:has_gpu约束。此外基础向量加法示例在 Apple GPU 上有已知问题TODO 注释 GEX-3548 记录 MEF 输出全零相关 target 在//:apple_gpu下被显式标记为不兼容。构建与验证方式小结运行方式命令 / 目标说明Pixi 任务pixi run test基础向量加法全流程需 GPUPixi 任务pixi run test-v3新版export_mef()API 流程需 GPUPixi 任务pixi run test-weights外部权重注册CPU 即可Bazel 测试//max/examples/capi:test、:test_v3、:test_weights、:test_safetensors、:test_graph_capture见 BUILD.bazel均声明为testonly三个核心工程约束值得在生产接入时牢记命名契约图输入/输出命名input0/input1/output0与外部权重命名必须与 C 端读取的名字严格一致Safetensors 场景尤其如此MEF 设备相关MEF 只能在相似系统间迁移跨 GPU 型号或驱动版本迁移前需重新编译导出设备依赖向量加法与图捕获示例要求 GPU且图捕获仅支持 CUDA/HIP外部权重与 Safetensors 示例可在 CPU 上完整运行。通过pixi run test一条命令即可复现从 Python 图构建、编译导出、到 C 程序加载执行与结果校验的完整闭环——这套编译与执行分离的架构正是 MAX 平台将模型部署进 C/C 服务端的关键路径。【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价