refactor: unify elementwise runtime helpers

This commit is contained in:
yutianyu 2026-05-12 16:23:06 +08:00
parent 9271fed062
commit c8ad11c277
13 changed files with 395 additions and 261 deletions

View File

@ -3,8 +3,8 @@
This repository is a training-oriented GPU operator runtime. It is intentionally
small, but its workflow mirrors production operator libraries:
1. Create a directory under `ops/<op>/` with backend implementations, or under
`ops/elementwise/<op>/` when the operator can reuse the elementwise framework.
1. Create a custom backend implementation under `ops/<op>/`, or place the
operator under `ops/elementwise/<op>/` when it should reuse the elementwise framework.
2. The build system auto-discovers sources by directory convention.
3. Implement a backend-specific descriptor lifecycle (create, workspace, execute, destroy).
4. Expose a Python API with out-of-place, out-variant, and prepared execution.
@ -64,11 +64,16 @@ python tests/run_ops.py --op all --backend nvidia --mode bench
./scripts/build_metax.sh test
```
The TileLang backend requires the `tilelang` Python package. The MetaX backend is built as a separate variant, should use `CAMP_ENABLE_NVIDIA=OFF`, and stores backend sources under `ops/*/metax/*.maca` or `ops/elementwise/*/metax/*.maca`.
The TileLang backend requires the `tilelang` Python package. The MetaX backend is built as a separate variant, should use `CAMP_ENABLE_NVIDIA=OFF`, and stores backend sources under `ops/*/metax/*.maca` or `ops/elementwise/*/metax/*.maca`. The C ABI exposes a unified backend-selection interface; each build artifact accepts only the backend compiled into it, and returns `not supported` for unavailable backends.
## Elementwise Framework
Elementwise operators that share the common shape/stride/broadcast execution model should live under `ops/elementwise/<op>/<backend>/`. The shared NVIDIA launcher is in `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`; each operator only needs to provide its public C API, descriptor lifecycle, dtype dispatch, and a small device functor. `relu` is the teaching example: `negative_slope=0.0` behaves like standard ReLU, while non-zero values behave like leaky ReLU.
The training camp supports two operator paths:
- Custom operators live under `ops/<op>/<backend>/`. This path is useful for teaching hand-written kernels, fixed contiguous fast paths, custom workspace needs, or non-elementwise structures. `copy` and `vector_add` demonstrate this path.
- Reusable elementwise operators live under `ops/elementwise/<op>/<backend>/`. This path is useful for ordinary elementwise operators that share the shape/stride/broadcast execution model. Later exercises can ask students to reimplement `copy` / `add`-style operators with this path.
The shared NVIDIA launcher is in `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`; shared descriptor helpers live in `include/operator_runtime/detail/elementwise.h`. Each elementwise operator usually only needs to provide its public C API, small dtype dispatch, and a device functor. On the Python side, use `ElementwiseOpSpec` to describe input count, scalar parameters, and broadcast semantics. `relu` is the teaching example: `negative_slope=0.0` behaves like standard ReLU, while non-zero values behave like leaky ReLU.
## Production Mapping

View File

@ -2,7 +2,7 @@
这个仓库是一个面向训练的 GPU 算子运行时。它体积很小,但工作流尽量贴近真实的算子库开发流程:
1. 在 `ops/<op>/` 下创建后端实现目录;如果算子可以复用 elementwise 框架,则放到 `ops/elementwise/<op>/`
1. 在 `ops/<op>/` 下创建自定义后端实现目录;如果算子选择复用 elementwise 框架,则放到 `ops/elementwise/<op>/`
2. 构建系统通过目录约定自动发现源码。
3. 实现后端专属的 descriptor 生命周期create、workspace、execute、destroy
4. 提供 Python API支持 out-of-place、out-variant 和 prepared 执行。
@ -62,11 +62,16 @@ python tests/run_ops.py --op all --backend nvidia --mode bench
./scripts/build_metax.sh test
```
TileLang 后端需要安装 `tilelang` Python 包。MetaX 后端使用独立构建产物,构建时应关闭 NVIDIA 变体,并将后端源码放在 `ops/*/metax/*.maca``ops/elementwise/*/metax/*.maca`
TileLang 后端需要安装 `tilelang` Python 包。MetaX 后端使用独立构建产物,构建时应关闭 NVIDIA 变体,并将后端源码放在 `ops/*/metax/*.maca``ops/elementwise/*/metax/*.maca`C ABI 暴露统一的 backend 选择接口;当前构建产物只接受已编译进来的 backend请求未启用 backend 会返回 `not supported`
## Elementwise 框架
普通逐元素算子如果共享 shape、stride、broadcast 的执行模型,推荐放在 `ops/elementwise/<op>/<backend>/`。NVIDIA 公共 launcher 位于 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`;每个具体算子只需要提供公开 C API、descriptor 生命周期、dtype dispatch 和一个小的 device functor。`relu` 是教学示例:`negative_slope=0.0` 时等价于标准 ReLU非 0 时等价于 leaky ReLU。
训练营支持两条路径:
- 自定义算子路径:放在 `ops/<op>/<backend>/`,适合教学手写 kernel、固定 contiguous fast path、特殊 workspace 或非逐元素结构。`copy` 和 `vector_add` 是这条路径的演示。
- elementwise 复用路径:放在 `ops/elementwise/<op>/<backend>/`,适合共享 shape、stride、broadcast 执行模型的普通逐元素算子。后续作业可以让学生用这条路径重新实现 `copy` / `add` 类算子。
NVIDIA 公共 launcher 位于 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`;公共 descriptor helper 位于 `include/operator_runtime/detail/elementwise.h`。每个 elementwise 算子通常只需要提供公开 C API、少量 dtype dispatch 和一个 device functor。Python 侧优先用 `ElementwiseOpSpec` 描述输入数、标量参数和 broadcast 语义。`relu` 是 elementwise 教学示例:`negative_slope=0.0` 时等价于标准 ReLU非 0 时等价于 leaky ReLU。
## 生产映射

View File

@ -2,9 +2,9 @@
## 目标
在当前项目里,新增一个可运行算子的最小闭环包括四部分:
在当前项目里,新增一个可运行算子的最小闭环包括四部分。训练营同时保留两条路径自定义算子路径用于教学手写 kernel 和特殊算子结构elementwise 复用路径用于普通逐元素算子。
1. 在 `ops/<算子名>/` 下补后端实现;普通逐元素算子优先放到 `ops/elementwise/<算子名>/`
1. 在 `ops/<算子名>/` 下补自定义后端实现,或在 `ops/elementwise/<算子名>/` 下补复用 elementwise 框架的实现
2. 在 `python/operator_runtime/ops/` 下补 Python API。
3. 在 `tests/` 下补正确性测试和 benchmark 入口。
4. 重新构建并验证。
@ -21,16 +21,16 @@
5. 是否需要额外参数,例如 `dim`、`scalar`。
6. 是否需要 workspace。
这一步的目的,是确定后面 C API 和 Python 绑定该按哪种模式实现。
这一步的目的,是确定后面 C API 和 Python 绑定该按哪种模式实现。不要把“逐元素”简单等同于“必须用 elementwise 框架”:训练营会先用 `copy`、`vector_add` 展示自定义算子写法,再把复用 elementwise 框架作为后续作业和工程化路径。
## Step 2创建算子目录
先选择算子目录:
- 普通手写 kernel放在 `ops/<算子名>/`
- 可复用公共 shape/stride/broadcast 逐元素框架的算子:放在 `ops/elementwise/<算子名>/`
- 自定义算子:放在 `ops/<算子名>/`。适合教学底层 kernel、固定 contiguous fast path、特殊 workspace、reduce/softmax 等非普通 elementwise 算子
- elementwise 复用算子:放在 `ops/elementwise/<算子名>/`适合共享 shape、stride、broadcast 执行模型的 unary / binary / 多输入逐元素算子。
`relu` 是 elementwise 框架示例,目录为 `ops/elementwise/relu/nvidia/`,公共 NVIDIA launcher 位于 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`
`copy` 和 `vector_add` 是自定义算子教学示例。`relu` 是 elementwise 框架示例,目录为 `ops/elementwise/relu/nvidia/`,公共 NVIDIA launcher 位于 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`
先在选定目录下建立对应后端目录。
@ -49,7 +49,7 @@
`ops/<算子名>/nvidia/``ops/elementwise/<算子名>/nvidia/` 这一层负责 C++/CUDA 实现。
普通手写 kernel 通常需要这几部分:
自定义算子通常需要这几部分:
1. kernel 文件
2. C API 头文件
@ -57,11 +57,11 @@
这里最关键的是保持现有命名约定一致,因为 Python 侧会按固定符号名去找函数。
如果是 elementwise 算子,不需要每个算子重复写 shape/stride kernel。推荐复用 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`
如果是 elementwise 复用算子,不需要每个算子重复写 shape/stride kernel。推荐复用 `include/operator_runtime/detail/elementwise.h` 里的 descriptor helper 和 `ops/common/elementwise/nvidia/elementwise_nvidia.cuh`
1. descriptor 中保存 `oprt::ElementwiseInfo`。
2. create 阶段调用 `oprt::create_elementwise_info(out, {inputs...}, &info)`。
3. workspace 返回 `info.workspace_bytes()`。
1. descriptor 继承 `oprt::ElementwiseDescriptorBase`。
2. create 阶段调用 `oprt::init_elementwise_descriptor(desc, out, {inputs...})`。
3. workspace 复用 `oprt::get_elementwise_workspace_size`。
4. execute 阶段只做 dtype dispatch并调用 `oprt::elementwise::nvidia::launch<T, N>(...)`
5. 算子自身只提供 device functor例如 `relu``value > 0 ? value : value * negative_slope`
@ -95,8 +95,18 @@ Python 入口放在 `python/operator_runtime/ops/<算子名>.py`。
2. `<算子名>_`:接收调用方提供的输出 tensor执行一次。
3. `<算子名>`:自动分配输出 tensor再调用 `<算子名>_`
如果你的算子签名和现有 unary、binary、reduce-like 模式一致,就沿用现有 helper。
如果签名比较特殊,例如带额外标量参数,就单独写一层绑定。
如果你的算子是自定义算子,并且签名和现有 unary、binary、reduce-like 模式一致,就沿用现有 helper。
如果你的算子是 elementwise 复用算子,优先在 Python 里定义 `ElementwiseOpSpec`,再调用 `prepare_elementwise_op`。这个 spec 描述算子名、输入数、标量参数和 broadcast 语义,避免为每个 elementwise 算子重复写 FFI 绑定和基础校验。
示例:
```python
_MY_OP_SPEC = ElementwiseOpSpec(
name="my_op",
input_count=1,
scalar_argtypes=(ctypes.c_float,),
)
```
## Step 5导出到公共 API
@ -191,13 +201,13 @@ API contract 测试主要覆盖 shape 不匹配、dtype 不匹配、非 contiguo
## 现有模板怎么选
- `copy`:适合最简单的单输入单输出流程。
- `vector_add`:适合标准 contiguous 双输入算子。
- `copy`:适合最简单的自定义单输入单输出流程。
- `vector_add`:适合标准 contiguous 双输入自定义算子。
- `relu`:适合复用 elementwise 框架的单输入逐元素算子,也展示了额外标量参数 `negative_slope` 的 C/Python 绑定方式。
- `reduce_sum`:适合带 reduce 维度的算子。
- `softmax`:适合带更明确 shape 约束和归一化逻辑的算子。
如果新算子本质上是普通 elementwise优先参考 `relu``ops/common/elementwise/nvidia/elementwise_nvidia.cuh`;如果只想写最简单 contiguous kernel再参考 `vector_add`
如果新算子本质上是普通 elementwise优先参考 `relu`、`ElementwiseOpSpec``ops/common/elementwise/nvidia/elementwise_nvidia.cuh`;如果课程目标是练习手写 contiguous kernel再参考 `copy``vector_add` 的自定义路径
## 最终检查清单

View File

@ -27,6 +27,12 @@ typedef enum {
OPRT_DTYPE_F32 = 1
} oprt_dtype_t;
typedef enum {
OPRT_BACKEND_AUTO = 0,
OPRT_BACKEND_NVIDIA = 1,
OPRT_BACKEND_METAX = 2
} oprt_backend_t;
#define OPRT_MAX_DIMS 8
typedef void *oprt_stream_t;
@ -41,8 +47,10 @@ typedef struct {
} oprt_tensor_view_t;
OPRT_EXPORT const char *oprt_status_string(oprt_status_t status);
OPRT_EXPORT const char *oprt_backend_string(oprt_backend_t backend);
OPRT_EXPORT oprt_status_t oprt_set_backend(oprt_backend_t backend);
OPRT_EXPORT oprt_status_t oprt_get_backend(oprt_backend_t *backend);
#ifdef __cplusplus
}
#endif

View File

@ -10,7 +10,7 @@ struct oprt_operator_descriptor {
virtual ~oprt_operator_descriptor() = default;
virtual const char *op_name() const = 0;
size_t workspace_size = 0;
oprt_backend_t backend = OPRT_BACKEND_AUTO;
};
#endif

View File

@ -1,5 +1,6 @@
#pragma once
#include "operator_runtime/descriptor.h"
#include "operator_runtime/tensor_view.h"
#include "operator_runtime/detail/tensor_checks.h"
@ -181,6 +182,68 @@ inline oprt_status_t create_elementwise_info(const oprt_tensor_view_t *out,
return OPRT_SUCCESS;
}
struct ElementwiseDescriptorBase : oprt_operator_descriptor {
oprt_tensor_view_t out_view{};
std::vector<oprt_tensor_view_t> input_views;
oprt::ElementwiseInfo info;
};
inline oprt_status_t init_elementwise_descriptor(ElementwiseDescriptorBase *desc,
const oprt_tensor_view_t *out,
const std::vector<const oprt_tensor_view_t *> &inputs) {
if (desc == nullptr || out == nullptr || inputs.empty()) {
return OPRT_ERR_INVALID_ARG;
}
oprt::ElementwiseInfo info;
auto status = oprt::create_elementwise_info(out, inputs, &info);
if (status != OPRT_SUCCESS) {
return status;
}
desc->out_view = *out;
desc->input_views.clear();
desc->input_views.reserve(inputs.size());
for (const auto *input : inputs) {
desc->input_views.push_back(*input);
}
desc->info = std::move(info);
desc->workspace_size = desc->info.workspace_bytes();
return OPRT_SUCCESS;
}
inline oprt_status_t get_elementwise_workspace_size(oprt_operator_descriptor_t desc,
size_t *size) {
if (desc == nullptr || size == nullptr) {
return OPRT_ERR_INVALID_ARG;
}
*size = desc->workspace_size;
return OPRT_SUCCESS;
}
inline oprt_status_t validate_elementwise_execute_args(oprt_operator_descriptor_t desc,
size_t workspace_size,
void *out,
const std::vector<const void *> &inputs) {
if (desc == nullptr || out == nullptr || inputs.empty()) {
return OPRT_ERR_INVALID_ARG;
}
for (const auto *input : inputs) {
if (input == nullptr) {
return OPRT_ERR_INVALID_ARG;
}
}
if (workspace_size < desc->workspace_size) {
return OPRT_ERR_INSUFFICIENT_WORKSPACE;
}
return OPRT_SUCCESS;
}
inline oprt_status_t destroy_elementwise_descriptor(oprt_operator_descriptor_t desc) {
delete desc;
return OPRT_SUCCESS;
}
} // namespace oprt
#endif

View File

@ -1,5 +1,15 @@
#include "operator_runtime/api.h"
namespace {
#ifdef CAMP_ENABLE_NVIDIA
oprt_backend_t g_current_backend = OPRT_BACKEND_NVIDIA;
#elif defined(CAMP_ENABLE_METAX)
oprt_backend_t g_current_backend = OPRT_BACKEND_METAX;
#else
oprt_backend_t g_current_backend = OPRT_BACKEND_AUTO;
#endif
}
extern "C" OPRT_EXPORT const char *oprt_status_string(oprt_status_t status) {
switch (status) {
case OPRT_SUCCESS:
@ -19,3 +29,54 @@ extern "C" OPRT_EXPORT const char *oprt_status_string(oprt_status_t status) {
}
}
extern "C" OPRT_EXPORT const char *oprt_backend_string(oprt_backend_t backend) {
switch (backend) {
case OPRT_BACKEND_AUTO:
return "auto";
case OPRT_BACKEND_NVIDIA:
return "nvidia";
case OPRT_BACKEND_METAX:
return "metax";
default:
return "unknown";
}
}
extern "C" OPRT_EXPORT oprt_status_t oprt_set_backend(oprt_backend_t backend) {
switch (backend) {
case OPRT_BACKEND_AUTO:
#ifdef CAMP_ENABLE_NVIDIA
g_current_backend = OPRT_BACKEND_NVIDIA;
return OPRT_SUCCESS;
#elif defined(CAMP_ENABLE_METAX)
g_current_backend = OPRT_BACKEND_METAX;
return OPRT_SUCCESS;
#else
return OPRT_ERR_NOT_SUPPORTED;
#endif
case OPRT_BACKEND_NVIDIA:
#ifdef CAMP_ENABLE_NVIDIA
g_current_backend = backend;
return OPRT_SUCCESS;
#else
return OPRT_ERR_NOT_SUPPORTED;
#endif
case OPRT_BACKEND_METAX:
#ifdef CAMP_ENABLE_METAX
g_current_backend = backend;
return OPRT_SUCCESS;
#else
return OPRT_ERR_NOT_SUPPORTED;
#endif
default:
return OPRT_ERR_INVALID_ARG;
}
}
extern "C" OPRT_EXPORT oprt_status_t oprt_get_backend(oprt_backend_t *backend) {
if (backend == nullptr) {
return OPRT_ERR_INVALID_ARG;
}
*backend = g_current_backend;
return OPRT_SUCCESS;
}

View File

@ -5,10 +5,7 @@
namespace oprt::elementwise {
struct ReluDescriptor final : oprt_operator_descriptor {
oprt_tensor_view_t out_view{};
oprt_tensor_view_t in_view{};
oprt::ElementwiseInfo info;
struct ReluDescriptor final : oprt::ElementwiseDescriptorBase {
float negative_slope = 0.0f;
const char *op_name() const override {
@ -30,29 +27,20 @@ inline oprt_status_t create_relu_descriptor_common(
return OPRT_ERR_INVALID_ARG;
}
oprt::ElementwiseInfo info;
auto status = oprt::create_elementwise_info(out, {in}, &info);
auto *typed = new ReluDescriptor();
auto status = oprt::init_elementwise_descriptor(typed, out, {in});
if (status != OPRT_SUCCESS) {
delete typed;
return status;
}
auto *typed = new ReluDescriptor();
typed->out_view = *out;
typed->in_view = *in;
typed->info = std::move(info);
typed->negative_slope = negative_slope;
typed->workspace_size = typed->info.workspace_bytes();
*desc = typed;
return OPRT_SUCCESS;
}
inline oprt_status_t get_relu_workspace_size_common(oprt_operator_descriptor_t desc,
size_t *size) {
if (desc == nullptr || size == nullptr) {
return OPRT_ERR_INVALID_ARG;
}
*size = desc->workspace_size;
return OPRT_SUCCESS;
return oprt::get_elementwise_workspace_size(desc, size);
}
inline oprt_status_t validate_relu_execute_args(oprt_operator_descriptor_t desc,
@ -62,15 +50,11 @@ inline oprt_status_t validate_relu_execute_args(oprt_operator_descriptor_t desc,
if (desc == nullptr || out == nullptr || in == nullptr) {
return OPRT_ERR_INVALID_ARG;
}
if (workspace_size < desc->workspace_size) {
return OPRT_ERR_INSUFFICIENT_WORKSPACE;
}
return OPRT_SUCCESS;
return oprt::validate_elementwise_execute_args(desc, workspace_size, out, {in});
}
inline oprt_status_t destroy_relu_descriptor_common(oprt_operator_descriptor_t desc) {
delete desc;
return OPRT_SUCCESS;
return oprt::destroy_elementwise_descriptor(desc);
}
} // namespace oprt::elementwise

View File

@ -5,6 +5,7 @@ from .bindings import (
Descriptor,
OperatorRuntimeError,
Status,
bind_elementwise,
bind_unary,
bind_relu,
bind_binary,
@ -24,6 +25,7 @@ __all__ = [
"Descriptor",
"OperatorRuntimeError",
"Status",
"bind_elementwise",
"bind_unary",
"bind_relu",
"bind_binary",

View File

@ -2,31 +2,50 @@ from __future__ import annotations
import ctypes
from dataclasses import dataclass
from typing import Callable
from typing import Callable, Sequence
from operator_runtime.backend import Backend
from operator_runtime.backend import normalize_backend
from .loader import load_library
from .tensor_view import TensorView
Status = ctypes.c_int
Descriptor = ctypes.c_void_p
OPRT_BACKEND_NVIDIA = 1
OPRT_BACKEND_METAX = 2
class OperatorRuntimeError(RuntimeError):
pass
def check_status(status: int) -> None:
def _backend_code(backend: str | Backend) -> int:
normalized = normalize_backend(backend)
if normalized is Backend.NVIDIA:
return OPRT_BACKEND_NVIDIA
if normalized is Backend.METAX:
return OPRT_BACKEND_METAX
raise NotImplementedError(f"backend {normalized.value} is not runnable through the C ABI")
def check_status(status: int, lib=None) -> None:
if status == 0:
return
lib = load_library()
if lib is None:
lib = load_library()
lib.oprt_status_string.argtypes = [ctypes.c_int]
lib.oprt_status_string.restype = ctypes.c_char_p
message = lib.oprt_status_string(status).decode("utf-8")
raise OperatorRuntimeError(message)
def set_runtime_backend(lib, backend: str | Backend) -> None:
lib.oprt_set_backend.argtypes = [ctypes.c_int]
lib.oprt_set_backend.restype = Status
check_status(lib.oprt_set_backend(_backend_code(backend)), lib)
@dataclass(frozen=True)
class CFunctions:
create: Callable
@ -41,14 +60,20 @@ def _missing_symbol_error(name: str, symbol: str, exc: AttributeError) -> Operat
) from exc
def bind_unary(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions:
lib = load_library()
def _bind_lifecycle(
name: str,
backend: str | Backend,
create_argtypes: Sequence[object],
execute_argtypes: Sequence[object],
) -> CFunctions:
lib = load_library(backend)
set_runtime_backend(lib, backend)
create_symbol = f"oprt_create_{name}_descriptor"
try:
create = getattr(lib, create_symbol)
except AttributeError as exc:
_missing_symbol_error(name, create_symbol, exc)
create.argtypes = [ctypes.POINTER(Descriptor), ctypes.POINTER(TensorView), ctypes.POINTER(TensorView)]
create.argtypes = [ctypes.POINTER(Descriptor), *create_argtypes]
create.restype = Status
workspace_symbol = f"oprt_get_{name}_workspace_size"
@ -64,14 +89,7 @@ def bind_unary(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions
execute = getattr(lib, execute_symbol)
except AttributeError as exc:
_missing_symbol_error(name, execute_symbol, exc)
execute.argtypes = [
Descriptor,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
]
execute.argtypes = [Descriptor, ctypes.c_void_p, ctypes.c_size_t, *execute_argtypes, ctypes.c_void_p]
execute.restype = Status
destroy_symbol = f"oprt_destroy_{name}_descriptor"
@ -84,147 +102,39 @@ def bind_unary(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions
return CFunctions(create, workspace, execute, destroy)
def bind_elementwise(
name: str,
input_count: int,
scalar_argtypes: Sequence[object] = (),
backend: str | Backend = Backend.NVIDIA,
) -> CFunctions:
if input_count <= 0:
raise ValueError("elementwise operators require at least one input")
tensor_views = [ctypes.POINTER(TensorView)] * (input_count + 1)
data_ptrs = [ctypes.c_void_p] * (input_count + 1)
return _bind_lifecycle(name, backend, [*tensor_views, *scalar_argtypes], data_ptrs)
def bind_unary(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions:
return bind_elementwise(name, 1, backend=backend)
def bind_relu(backend: str | Backend = Backend.NVIDIA) -> CFunctions:
lib = load_library()
name = "relu"
create_symbol = "oprt_create_relu_descriptor"
try:
create = getattr(lib, create_symbol)
except AttributeError as exc:
_missing_symbol_error(name, create_symbol, exc)
create.argtypes = [
ctypes.POINTER(Descriptor),
ctypes.POINTER(TensorView),
ctypes.POINTER(TensorView),
ctypes.c_float,
]
create.restype = Status
workspace_symbol = "oprt_get_relu_workspace_size"
try:
workspace = getattr(lib, workspace_symbol)
except AttributeError as exc:
_missing_symbol_error(name, workspace_symbol, exc)
workspace.argtypes = [Descriptor, ctypes.POINTER(ctypes.c_size_t)]
workspace.restype = Status
execute_symbol = "oprt_execute_relu"
try:
execute = getattr(lib, execute_symbol)
except AttributeError as exc:
_missing_symbol_error(name, execute_symbol, exc)
execute.argtypes = [
Descriptor,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
]
execute.restype = Status
destroy_symbol = "oprt_destroy_relu_descriptor"
try:
destroy = getattr(lib, destroy_symbol)
except AttributeError as exc:
_missing_symbol_error(name, destroy_symbol, exc)
destroy.argtypes = [Descriptor]
destroy.restype = Status
return CFunctions(create, workspace, execute, destroy)
return bind_elementwise("relu", 1, [ctypes.c_float], backend)
def bind_binary(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions:
lib = load_library()
create_symbol = f"oprt_create_{name}_descriptor"
try:
create = getattr(lib, create_symbol)
except AttributeError as exc:
_missing_symbol_error(name, create_symbol, exc)
create.argtypes = [
ctypes.POINTER(Descriptor),
ctypes.POINTER(TensorView),
ctypes.POINTER(TensorView),
ctypes.POINTER(TensorView),
]
create.restype = Status
workspace_symbol = f"oprt_get_{name}_workspace_size"
try:
workspace = getattr(lib, workspace_symbol)
except AttributeError as exc:
_missing_symbol_error(name, workspace_symbol, exc)
workspace.argtypes = [Descriptor, ctypes.POINTER(ctypes.c_size_t)]
workspace.restype = Status
execute_symbol = f"oprt_execute_{name}"
try:
execute = getattr(lib, execute_symbol)
except AttributeError as exc:
_missing_symbol_error(name, execute_symbol, exc)
execute.argtypes = [
Descriptor,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
]
execute.restype = Status
destroy_symbol = f"oprt_destroy_{name}_descriptor"
try:
destroy = getattr(lib, destroy_symbol)
except AttributeError as exc:
_missing_symbol_error(name, destroy_symbol, exc)
destroy.argtypes = [Descriptor]
destroy.restype = Status
return CFunctions(create, workspace, execute, destroy)
return bind_elementwise(name, 2, backend=backend)
def bind_reduce_like(name: str, backend: str | Backend = Backend.NVIDIA) -> CFunctions:
lib = load_library()
create_symbol = f"oprt_create_{name}_descriptor"
try:
create = getattr(lib, create_symbol)
except AttributeError as exc:
_missing_symbol_error(name, create_symbol, exc)
create.argtypes = [
ctypes.POINTER(Descriptor),
ctypes.POINTER(TensorView),
ctypes.POINTER(TensorView),
ctypes.c_int64,
]
create.restype = Status
workspace_symbol = f"oprt_get_{name}_workspace_size"
try:
workspace = getattr(lib, workspace_symbol)
except AttributeError as exc:
_missing_symbol_error(name, workspace_symbol, exc)
workspace.argtypes = [Descriptor, ctypes.POINTER(ctypes.c_size_t)]
workspace.restype = Status
execute_symbol = f"oprt_execute_{name}"
try:
execute = getattr(lib, execute_symbol)
except AttributeError as exc:
_missing_symbol_error(name, execute_symbol, exc)
execute.argtypes = [
Descriptor,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
]
execute.restype = Status
destroy_symbol = f"oprt_destroy_{name}_descriptor"
try:
destroy = getattr(lib, destroy_symbol)
except AttributeError as exc:
_missing_symbol_error(name, destroy_symbol, exc)
destroy.argtypes = [Descriptor]
destroy.restype = Status
return CFunctions(create, workspace, execute, destroy)
return _bind_lifecycle(
name,
backend,
[
ctypes.POINTER(TensorView),
ctypes.POINTER(TensorView),
ctypes.c_int64,
],
[ctypes.c_void_p, ctypes.c_void_p],
)

View File

@ -6,28 +6,67 @@ from functools import lru_cache
from pathlib import Path
def _candidate_library_paths() -> list[Path]:
def _backend_key(backend: object | None) -> str | None:
if backend is None:
return None
from operator_runtime.backend import normalize_backend
return normalize_backend(backend).value
def _append_build_dir(candidates: list[Path], build_dir: str | None) -> None:
if not build_dir:
return
root = Path(build_dir)
candidates.extend([root / "libcamp_ops.so", root / "ops" / "libcamp_ops.so"])
def _candidate_library_paths(backend: str | None = None) -> list[Path]:
candidates: list[Path] = []
build_dir = os.environ.get("CAMP_BUILD_DIR")
if build_dir:
root = Path(build_dir)
candidates.extend([root / "libcamp_ops.so", root / "ops" / "libcamp_ops.so"])
repo_root = Path(__file__).resolve().parents[3]
candidates.extend(
[
repo_root / "build" / "libcamp_ops.so",
repo_root / "build" / "ops" / "libcamp_ops.so",
repo_root / "build-metax" / "libcamp_ops.so",
repo_root / "build-metax" / "ops" / "libcamp_ops.so",
]
)
if backend == "nvidia":
_append_build_dir(candidates, os.environ.get("CAMP_NVIDIA_BUILD_DIR"))
_append_build_dir(candidates, os.environ.get("CAMP_BUILD_DIR"))
candidates.extend(
[
repo_root / "build" / "libcamp_ops.so",
repo_root / "build" / "ops" / "libcamp_ops.so",
]
)
elif backend == "metax":
_append_build_dir(candidates, os.environ.get("CAMP_METAX_BUILD_DIR"))
_append_build_dir(candidates, os.environ.get("CAMP_BUILD_DIR"))
candidates.extend(
[
repo_root / "build-metax" / "libcamp_ops.so",
repo_root / "build-metax" / "ops" / "libcamp_ops.so",
]
)
else:
_append_build_dir(candidates, os.environ.get("CAMP_BUILD_DIR"))
_append_build_dir(candidates, os.environ.get("CAMP_NVIDIA_BUILD_DIR"))
_append_build_dir(candidates, os.environ.get("CAMP_METAX_BUILD_DIR"))
candidates.extend(
[
repo_root / "build" / "libcamp_ops.so",
repo_root / "build" / "ops" / "libcamp_ops.so",
repo_root / "build-metax" / "libcamp_ops.so",
repo_root / "build-metax" / "ops" / "libcamp_ops.so",
]
)
return candidates
@lru_cache(maxsize=1)
def load_library() -> ctypes.CDLL:
for path in _candidate_library_paths():
@lru_cache(maxsize=None)
def _load_library(backend: str | None) -> ctypes.CDLL:
for path in _candidate_library_paths(backend):
if path.exists():
return ctypes.CDLL(str(path))
searched = ", ".join(str(path) for path in _candidate_library_paths())
raise FileNotFoundError(f"libcamp_ops.so not found; searched: {searched}")
searched = ", ".join(str(path) for path in _candidate_library_paths(backend))
label = "default" if backend is None else backend
raise FileNotFoundError(f"libcamp_ops.so for backend {label} not found; searched: {searched}")
def load_library(backend: object | None = None) -> ctypes.CDLL:
return _load_library(_backend_key(backend))

View File

@ -1,10 +1,31 @@
from __future__ import annotations
import ctypes
from dataclasses import dataclass
from typing import Callable, Sequence
import torch
from operator_runtime._internal import CFunctions, Descriptor, PreparedOp, check_status
from operator_runtime.backend import Backend, normalize_backend
from operator_runtime._internal import CFunctions, Descriptor, PreparedOp, bind_elementwise, check_status, tensor_view
def _broadcastable_to(src_shape: tuple[int, ...], out_shape: tuple[int, ...]) -> bool:
if len(src_shape) > len(out_shape):
return False
offset = len(out_shape) - len(src_shape)
for out_dim, out_extent in enumerate(out_shape):
src_dim = out_dim - offset
if src_dim < 0:
continue
src_extent = src_shape[src_dim]
if src_extent != out_extent and src_extent != 1:
return False
return True
def _has_broadcast_dim(tensor: torch.Tensor) -> bool:
return any(size > 1 and stride == 0 for size, stride in zip(tensor.shape, tensor.stride()))
def build_prepared_op(
@ -22,3 +43,57 @@ def build_prepared_op(
workspace = torch.empty(workspace_size.value, dtype=torch.uint8, device=stream_tensor.device)
runner_args = tuple(ctypes.c_void_p(tensor.data_ptr()) for tensor in tensors)
return PreparedOp(funcs, desc, workspace, runner_args, stream_tensor)
@dataclass(frozen=True)
class ElementwiseOpSpec:
name: str
input_count: int
scalar_argtypes: tuple[object, ...] = ()
allow_broadcast: bool = True
def check_elementwise_tensors(spec: ElementwiseOpSpec, out: torch.Tensor, inputs: Sequence[torch.Tensor]) -> None:
if len(inputs) != spec.input_count:
raise ValueError(f"{spec.name} expects {spec.input_count} inputs, got {len(inputs)}")
if not out.is_cuda or any(not tensor.is_cuda for tensor in inputs):
raise ValueError(f"{spec.name} expects CUDA tensors")
if _has_broadcast_dim(out):
raise ValueError(f"{spec.name} expects a writable output tensor")
for tensor in inputs:
if spec.allow_broadcast:
if not _broadcastable_to(tuple(tensor.shape), tuple(out.shape)):
raise ValueError(f"{spec.name} expects inputs to be broadcastable to out")
elif out.shape != tensor.shape:
raise ValueError(f"{spec.name} expects matching shapes")
if out.dtype != tensor.dtype:
raise TypeError(f"{spec.name} expects matching dtypes")
def prepare_elementwise_op(
spec: ElementwiseOpSpec,
out: torch.Tensor,
inputs: Sequence[torch.Tensor],
scalars: Sequence[object] = (),
backend: str | Backend = Backend.NVIDIA,
scalar_converters: Sequence[Callable[[object], object]] = (),
) -> PreparedOp:
backend = normalize_backend(backend)
if backend not in (Backend.NVIDIA, Backend.METAX):
raise NotImplementedError(f"backend {backend.value} is not runnable")
check_elementwise_tensors(spec, out, inputs)
if len(scalars) != len(spec.scalar_argtypes):
raise ValueError(f"{spec.name} expects {len(spec.scalar_argtypes)} scalar args, got {len(scalars)}")
if scalar_converters and len(scalar_converters) != len(scalars):
raise ValueError("scalar_converters must match scalars")
funcs = bind_elementwise(spec.name, spec.input_count, spec.scalar_argtypes, backend)
views = [tensor_view(out), *(tensor_view(tensor) for tensor in inputs)]
converters: Sequence[Callable[[object], object]] = scalar_converters or tuple(lambda value: value for _ in scalars)
converted_scalars = [
argtype(converter(value))
for value, argtype, converter in zip(scalars, spec.scalar_argtypes, converters)
]
create_args = (*[ctypes.byref(view) for view in views], *converted_scalars)
return build_prepared_op(funcs, create_args, (out, *inputs), out)

View File

@ -4,38 +4,16 @@ import ctypes
import torch
from operator_runtime.backend import Backend, normalize_backend
from operator_runtime._internal import PreparedOp, bind_relu, tensor_view
from operator_runtime.ops._common import build_prepared_op
from operator_runtime.backend import Backend
from operator_runtime._internal import PreparedOp
from operator_runtime.ops._common import ElementwiseOpSpec, prepare_elementwise_op
def _broadcastable_to(src_shape: tuple[int, ...], out_shape: tuple[int, ...]) -> bool:
if len(src_shape) > len(out_shape):
return False
offset = len(out_shape) - len(src_shape)
for out_dim, out_extent in enumerate(out_shape):
src_dim = out_dim - offset
if src_dim < 0:
continue
src_extent = src_shape[src_dim]
if src_extent != out_extent and src_extent != 1:
return False
return True
def _has_broadcast_dim(tensor: torch.Tensor) -> bool:
return any(size > 1 and stride == 0 for size, stride in zip(tensor.shape, tensor.stride()))
def _check(out: torch.Tensor, src: torch.Tensor) -> None:
if not out.is_cuda or not src.is_cuda:
raise ValueError("relu expects CUDA tensors")
if _has_broadcast_dim(out):
raise ValueError("relu expects a writable output tensor")
if not _broadcastable_to(tuple(src.shape), tuple(out.shape)):
raise ValueError("relu expects src to be broadcastable to out")
if out.dtype != src.dtype:
raise TypeError("relu expects matching dtypes")
_RELU_SPEC = ElementwiseOpSpec(
name="relu",
input_count=1,
scalar_argtypes=(ctypes.c_float,),
)
def prepare_relu(
@ -44,20 +22,14 @@ def prepare_relu(
negative_slope: float = 0.0,
backend: str | Backend = Backend.NVIDIA,
) -> PreparedOp:
backend = normalize_backend(backend)
_check(out, src)
if backend not in (Backend.NVIDIA, Backend.METAX):
raise NotImplementedError(f"backend {backend.value} is not runnable")
funcs = bind_relu(backend)
out_view = tensor_view(out)
src_view = tensor_view(src)
create_args = (
ctypes.byref(out_view),
ctypes.byref(src_view),
ctypes.c_float(float(negative_slope)),
return prepare_elementwise_op(
_RELU_SPEC,
out,
(src,),
(negative_slope,),
backend,
(float,),
)
return build_prepared_op(funcs, create_args, (out, src), out)
def relu_(