示例 03:图捕获、编译与执行
本页是02 核心概念的完整可运行源码。
// =============================================================================
// 示例 02:图编译执行。
//
// 学习目标:完成建图、编译、整图执行与确定结果校验。
// 前置章节:help/02-core-concepts/README.md。
// 预期 PASS:打印所选后端及匹配期望值的 PASS 行。
// 运行边界:默认使用 CPU;传入 cuda 时真实执行 H2D、CUDA 整图与 D2H。
// =============================================================================
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <memory>
#include <string_view>
#include <utility>
#include <vector>
#include <frame/core/device.h>
#include <frame/core/dtype.h>
#include <frame/core/shape.h>
#include <frame/core/status.h>
#include <frame/core/tensor.h>
#include <frame/hal/allocator.h>
#include <frame/hal/backend.h>
#include <frame/hal/executable.h>
#include <frame/hal/stream.h>
#include <frame/ir/graph.h>
#include <frame/ir/node.h>
#include <frame/runtime/compile.h>
namespace {
// 打印失败 Status 到 stderr 并返回 false;调用方据此立即返回。
bool check_example_02_status(const frame::Status& status, std::string_view operation) {
if (status.is_ok()) return true;
std::cerr << operation << " failed: " << status.message() << "\n";
return false;
}
// TensorType 的 device 必须与本次选择的编译后端一致。
frame::ir::TensorType make_example_02_tensor_type(std::vector<int64_t> dims, frame::Device device) {
frame::ir::TensorType type;
type.dtype = frame::DType::of<float>();
type.shape = frame::Shape(std::move(dims));
type.device = device;
return type;
}
} // namespace
int main(int argc, char** argv) {
// ---- 1. 解析后端参数。----
if (argc > 2) {
std::cerr << "usage: frame_example_02_graph_compile [cpu|cuda]\n";
return 1;
}
const std::string_view requested_backend = argc == 2 ? argv[1] : frame::kCpuBackendName;
if (requested_backend != frame::kCpuBackendName && requested_backend != frame::kCudaBackendName) {
std::cerr << "backend must be either 'cpu' or 'cuda'\n";
return 1;
}
const bool use_cuda = requested_backend == frame::kCudaBackendName;
const std::string_view backend_name = use_cuda ? frame::kCudaBackendName : frame::kCpuBackendName;
const frame::Device execution_device =
use_cuda ? frame::Device{frame::kCudaBackendName, 0} : frame::cpu_device();
// ---- 2. 取得目标后端,并确认目标设备存在。----
const frame::Result<frame::hal::Backend*> backend_result =
frame::hal::BackendRegistry::instance().get(backend_name);
if (!backend_result.is_ok()) {
std::cerr << "failed to get backend '" << backend_name
<< "': " << backend_result.status().message() << "\n";
return 1;
}
frame::hal::Backend* execution_backend = backend_result.value();
const frame::Result<int32_t> device_count_result = execution_backend->device_count();
if (!device_count_result.is_ok()) {
std::cerr << "failed to query device count: " << device_count_result.status().message() << "\n";
return 1;
}
if (device_count_result.value() <= execution_device.index) {
std::cerr << "backend '" << backend_name << "' has no device 0\n";
return 1;
}
frame::hal::Allocator* execution_allocator = execution_backend->allocator(execution_device);
if (execution_allocator == nullptr) {
std::cerr << "backend '" << backend_name << "' returned a null allocator\n";
return 1;
}
// CPU allocator 始终用于 host staging;CUDA 路径不会直接读写 device 指针。
const frame::Result<frame::hal::Backend*> cpu_backend_result =
frame::hal::BackendRegistry::instance().get(frame::kCpuBackendName);
if (!cpu_backend_result.is_ok()) {
std::cerr << "failed to get cpu staging backend: " << cpu_backend_result.status().message()
<< "\n";
return 1;
}
frame::hal::Backend* cpu_backend = cpu_backend_result.value();
const frame::Device cpu_device = frame::cpu_device();
frame::hal::Allocator* cpu_allocator = cpu_backend->allocator(cpu_device);
if (cpu_allocator == nullptr) {
std::cerr << "cpu backend returned a null staging allocator\n";
return 1;
}
// H2D/D2H 是异步 copy,CUDA 路径显式创建传输流并在每个传输阶段同步。
std::unique_ptr<frame::hal::Stream> transfer_stream;
if (use_cuda) {
frame::Result<std::unique_ptr<frame::hal::Stream>> stream_result =
execution_backend->create_stream(execution_device);
if (!stream_result.is_ok()) {
std::cerr << "failed to create cuda transfer stream: " << stream_result.status().message()
<< "\n";
return 1;
}
transfer_stream = std::move(stream_result.value());
if (transfer_stream == nullptr) {
std::cerr << "cuda backend returned a null transfer stream\n";
return 1;
}
}
// ---- 3. 建图:matmul([2,3]x[3,4]) -> add(+bias[2,4]) -> relu。----
frame::ir::Graph graph("matmul_add_relu_example");
const frame::Result<frame::ir::Value*> x_result =
graph.add_graph_input(make_example_02_tensor_type({2, 3}, execution_device));
if (!check_example_02_status(x_result.status(), "add_graph_input(x)")) return 1;
const frame::Result<frame::ir::Value*> w_result =
graph.add_graph_input(make_example_02_tensor_type({3, 4}, execution_device));
if (!check_example_02_status(w_result.status(), "add_graph_input(w)")) return 1;
const frame::Result<frame::ir::Value*> bias_result =
graph.add_graph_input(make_example_02_tensor_type({2, 4}, execution_device));
if (!check_example_02_status(bias_result.status(), "add_graph_input(bias)")) return 1;
const frame::Result<frame::ir::Node*> matmul_result =
graph.create_node("matmul", {x_result.value(), w_result.value()},
{make_example_02_tensor_type({2, 4}, execution_device)});
if (!check_example_02_status(matmul_result.status(), "create_node(matmul)")) return 1;
const frame::Result<frame::ir::Node*> add_result =
graph.create_node("add", {matmul_result.value()->output(0), bias_result.value()},
{make_example_02_tensor_type({2, 4}, execution_device)});
if (!check_example_02_status(add_result.status(), "create_node(add)")) return 1;
const frame::Result<frame::ir::Node*> relu_result =
graph.create_node("relu", {add_result.value()->output(0)},
{make_example_02_tensor_type({2, 4}, execution_device)});
if (!check_example_02_status(relu_result.status(), "create_node(relu)")) return 1;
if (!check_example_02_status(graph.mark_output(relu_result.value(), 0), "mark_output")) return 1;
// runtime::compile 是统一编译入口。CUDA 参数下传入 "cuda",图内 device 也
// 已是 Device{"cuda",0},因此不会把 CPU 图伪装成 CUDA 执行。
const frame::Result<std::shared_ptr<frame::hal::Executable>> executable_result =
frame::runtime::compile(graph, backend_name, frame::hal::CompileOptions{});
if (!check_example_02_status(executable_result.status(), "runtime::compile")) return 1;
// ---- 4. 在 CPU staging Tensor 中填写确定输入。----
const frame::Result<frame::Tensor> x_host_result = frame::Tensor::empty(
frame::Shape({2, 3}), frame::DType::of<float>(), cpu_device, *cpu_allocator);
if (!check_example_02_status(x_host_result.status(), "Tensor::empty(x_host)")) return 1;
frame::Tensor x_host = x_host_result.value();
const std::vector<float> x_values{1.0F, -2.0F, 3.0F, -4.0F, 5.0F, -6.0F};
std::copy(x_values.begin(), x_values.end(), x_host.data<float>());
const frame::Result<frame::Tensor> w_host_result = frame::Tensor::empty(
frame::Shape({3, 4}), frame::DType::of<float>(), cpu_device, *cpu_allocator);
if (!check_example_02_status(w_host_result.status(), "Tensor::empty(w_host)")) return 1;
frame::Tensor w_host = w_host_result.value();
const std::vector<float> w_values{1.0F, 0.0F, 0.0F, 1.0F, 0.0F, 1.0F,
0.0F, 1.0F, 0.0F, 0.0F, 1.0F, 1.0F};
std::copy(w_values.begin(), w_values.end(), w_host.data<float>());
const frame::Result<frame::Tensor> bias_host_result = frame::Tensor::empty(
frame::Shape({2, 4}), frame::DType::of<float>(), cpu_device, *cpu_allocator);
if (!check_example_02_status(bias_host_result.status(), "Tensor::empty(bias_host)")) return 1;
frame::Tensor bias_host = bias_host_result.value();
std::fill(bias_host.data<float>(), bias_host.data<float>() + bias_host.numel(), 0.5F);
// CPU 路径可直接复用 staging Tensor;CUDA 路径为每个输入分配显存并排入 H2D。
const auto copy_to_execution_device =
[&](const frame::Tensor& host_tensor) -> frame::Result<frame::Tensor> {
if (!use_cuda) return host_tensor;
frame::Result<frame::Tensor> device_tensor_result = frame::Tensor::empty(
host_tensor.shape(), host_tensor.dtype(), execution_device, *execution_allocator);
if (!device_tensor_result.is_ok()) return device_tensor_result.status();
frame::Tensor device_tensor = device_tensor_result.value();
const size_t bytes = static_cast<size_t>(host_tensor.numel()) * host_tensor.dtype().itemsize();
const frame::Status copy_status =
execution_backend->copy(device_tensor.raw_data(), execution_device, host_tensor.raw_data(),
cpu_device, bytes, transfer_stream.get());
if (!copy_status.is_ok()) return copy_status;
return device_tensor;
};
const frame::Result<frame::Tensor> x_input_result = copy_to_execution_device(x_host);
const frame::Result<frame::Tensor> w_input_result = copy_to_execution_device(w_host);
const frame::Result<frame::Tensor> bias_input_result = copy_to_execution_device(bias_host);
if (!x_input_result.is_ok() || !w_input_result.is_ok() || !bias_input_result.is_ok()) {
std::cerr << "failed to prepare execution inputs\n";
return 1;
}
if (use_cuda &&
!check_example_02_status(transfer_stream->synchronize(), "cuda H2D synchronize")) {
return 1;
}
// ---- 5. 执行整图。----
// run_with_allocated_outputs 在目标后端分配输出、创建执行流、执行并同步。
const std::vector<frame::Tensor> inputs{x_input_result.value(), w_input_result.value(),
bias_input_result.value()};
const frame::Result<std::vector<frame::Tensor>> outputs_result =
frame::runtime::run_with_allocated_outputs(*executable_result.value(), backend_name, inputs);
if (!check_example_02_status(outputs_result.status(), "run_with_allocated_outputs")) return 1;
if (outputs_result.value().size() != 1) {
std::cerr << "execution returned an unexpected output count\n";
return 1;
}
const frame::Tensor& execution_result = outputs_result.value()[0];
// CUDA 输出先 D2H 到新的 CPU Tensor。只有 result_host 会被 host 解引用。
frame::Tensor result_host = execution_result;
if (use_cuda) {
const frame::Result<frame::Tensor> host_result = frame::Tensor::empty(
execution_result.shape(), execution_result.dtype(), cpu_device, *cpu_allocator);
if (!check_example_02_status(host_result.status(), "Tensor::empty(result_host)")) return 1;
result_host = host_result.value();
const size_t bytes =
static_cast<size_t>(execution_result.numel()) * execution_result.dtype().itemsize();
const frame::Status copy_status =
execution_backend->copy(result_host.raw_data(), cpu_device, execution_result.raw_data(),
execution_device, bytes, transfer_stream.get());
if (!check_example_02_status(copy_status, "cuda D2H copy")) return 1;
if (!check_example_02_status(transfer_stream->synchronize(), "cuda D2H synchronize")) {
return 1;
}
}
// ---- 6. 打印并与手算期望值比对。----
std::cout << "backend: " << backend_name << "\n";
std::cout << "relu(matmul(x, w) + bias) =";
const float* result_data = static_cast<const float*>(result_host.raw_data());
for (int64_t i = 0; i < result_host.numel(); ++i) {
std::cout << " " << result_data[i];
}
std::cout << "\n";
const std::vector<float> expected{1.5F, 0.0F, 3.5F, 2.5F, 0.0F, 5.5F, 0.0F, 0.0F};
for (int64_t i = 0; i < result_host.numel(); ++i) {
// 本例仅使用整数与二进制可精确表示的 0.5,可直接做精确比较。
if (result_data[i] != expected[static_cast<size_t>(i)]) {
std::cerr << "result does not match the hand-computed expected value\n";
return 1;
}
}
std::cout << "PASS: compiled " << backend_name << " execution matches the expected value\n";
return 0;
}