Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

cmake_minimum_required(VERSION 3.18 FATAL_ERROR)

set(ACCELERATOR "cuda" CACHE STRING "Accelerator platform: cuda, metax, ascend, tsingmicro, dcu, gcu, or musa")
set(ACCELERATOR "cuda" CACHE STRING "Accelerator platform: cuda, metax, ascend, tsingmicro, dcu, gcu, musa, or bpu")

# Directory inside the wheel holding a bundled forked libtorch, when the backend
# ships one (see scripts/bundle_*_libtorch.sh). "lib" means "no separate bundle
Expand Down Expand Up @@ -95,6 +95,14 @@ elseif(ACCELERATOR STREQUAL "musa")
set(CUDA_KERNEL OFF CACHE BOOL "Build CUDA kernel implementations" FORCE)
set(FLAGGEMS_KERNEL OFF CACHE BOOL "Build FlagGems kernel implementations" FORCE)
project(TORCH_FLAGOS CXX C)
elseif(ACCELERATOR STREQUAL "bpu")
# D-Robotics RDK BPU. No CUDA runtime and no per-op kernels: the BPU executes
# whole compiled graphs, so eager ops fall back to CPU and acceleration comes
# from the torch.compile backend. Only the runtime layer (UCP allocator,
# device/stream stubs) is native, all plain host C++.
set(CUDA_KERNEL OFF CACHE BOOL "Build CUDA kernel implementations" FORCE)
set(FLAGGEMS_KERNEL OFF CACHE BOOL "Build FlagGems kernel implementations" FORCE)
project(TORCH_FLAGOS CXX C)
else()
# PPU rides this branch: it has no accelerator value of its own because it is
# built against PPU_SDK/CUDA_SDK, so the CUDA boxing kernels apply unchanged
Expand Down Expand Up @@ -133,7 +141,8 @@ set(CMAKE_CXX_EXTENSIONS OFF)

if(NOT ACCELERATOR STREQUAL "metax" AND NOT ACCELERATOR STREQUAL "ascend"
AND NOT ACCELERATOR STREQUAL "tsingmicro" AND NOT ACCELERATOR STREQUAL "dcu"
AND NOT ACCELERATOR STREQUAL "gcu" AND NOT ACCELERATOR STREQUAL "musa")
AND NOT ACCELERATOR STREQUAL "gcu" AND NOT ACCELERATOR STREQUAL "musa"
AND NOT ACCELERATOR STREQUAL "bpu")
set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()
Expand Down Expand Up @@ -376,6 +385,26 @@ elseif(ACCELERATOR STREQUAL "musa")
message(STATUS "mudnn op library: ${MUDNN_LIB}")
endif()

add_library(torch::cudart INTERFACE IMPORTED)
if(NOT TARGET CUDA::nvToolsExt)
add_library(CUDA::nvToolsExt INTERFACE IMPORTED)
endif()
elseif(ACCELERATOR STREQUAL "bpu")
# D-Robotics RDK BPU. There is no CUDA-compat layer here at all: device
# memory comes from the Horizon UCP allocator (libhbucp) and the core/task API
# from libbpu, both linked by csrc/runtime/accelerator/bpu directly. So there
# is no CUDA_RUNTIME_LIB to hand to the other targets.
set(HOBOT_LIB_DIR "/usr/hobot/lib" CACHE PATH "Horizon runtime library dir")
set(HOBOT_INCLUDE_DIR "/usr/include/hobot" CACHE PATH "Horizon runtime include dir")
message(STATUS "Horizon runtime: ${HOBOT_LIB_DIR} (headers ${HOBOT_INCLUDE_DIR})")

set(CUDA_RUNTIME_LIB "")

# Embed the Horizon library path so LD_LIBRARY_PATH is not needed at runtime.
list(APPEND CMAKE_INSTALL_RPATH "${HOBOT_LIB_DIR}")

# Pre-create torch::cudart so PyTorch's cuda.cmake returns early
# (guard: if(TARGET torch::cudart) return()).
add_library(torch::cudart INTERFACE IMPORTED)
if(NOT TARGET CUDA::nvToolsExt)
add_library(CUDA::nvToolsExt INTERFACE IMPORTED)
Expand Down
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,11 +609,62 @@ then rejects.
- Build with `-DMUSA_KERNEL=OFF` to skip mudnn entirely; the runtime still works
and all compute falls back to CPU.

### Build from Source (D-Robotics RDK BPU Platform)

The BPU is the one platform here with **no operator kernels at all**. Its BPU
executes whole compiled graphs (a `.hbm` produced by hbdk4), not individual ops,
so there is nothing for a `PrivateUse1` kernel to call. `torch_fl` supplies a
real device — UCP-backed memory plus the device/stream layer — and a
`torch.compile` backend; eager ops run on the CPU.

The supported target is **`torch==2.10.0+cpu`** (the cp314 aarch64 wheel on
PyPI is what the board runs), and `setup.py`'s `TORCH_PIN` enforces
`torch>=2.10,<2.11` here as on every other platform. That pin is not
incidental: the checked-in `csrc/aten/generated/*` bindings are generated
against one ATen surface, so a newer torch fails as a wall of compile errors
rather than a clean resolver error.

```bash
# Upstream CPU torch wheel; the Horizon runtime ships in the board image.
pip install torch==2.10.0+cpu --index-url https://download.pytorch.org/whl/cpu
ACCELERATOR=bpu pip install --no-build-isolation -vvv -e .
```

```python
import torch, torch_fl

compiled = torch.compile(MyNet().eval(), backend="bpu")
out = compiled(torch.randn(1, 3, 224, 224))
```

**BPU-specific notes:**

- **No SDK root to configure.** `libhbucp.so` (UCP allocator) and `libbpu.so`
(core/task API) ship at `/usr/hobot/lib` with headers at `/usr/include/hobot`.
- **hbdk4 compiles on the board, on the stock kernel.** The compiler ships
x86_64-only wheels, so it runs under box64 — which needs to be **built from
source**: the packaged 0.2.6 aborts on this board's 64 KB pages, while 0.4+
handles them at runtime. No VM, no cross-compile host, no kernel rebuild.
Run `scripts/setup_bpu_hbdk4.sh` once, then export
`FLAGOS_BPU_X86_PYTHON` and `FLAGOS_BPU_X86_EMULATOR`. Without a reachable
hbdk4 the backend logs a warning and runs every partition on the CPU, so the
install is still usable. Details in [docs/bpu.md](docs/bpu.md).
- **Quantization is a precondition, not an optimization.** hbdk4 lowers float
conv to the CPU, so int8 Q/DQ insertion is what puts work on the BPU at all.
On by default; `FLAGOS_BPU_QUANTIZE=0` for bit-exact float artifacts.
- **Convolution is registered explicitly.** `aten::convolution` routes
`PrivateUse1` to `convolution_overrideable`, whose only other kernel raises,
so the boxed fallback cannot reach a CPU implementation. Two wrappers in
`register.cc` call `at::convolution` on CPU tensors instead.
- Measured on-board (torch 2.10): a 6-layer conv stack at 224x224 runs **3.75 ms
on the BPU vs 72.06 ms eager CPU — 19.2x**. Toy nets are a wash — submission
overhead dominates.

### Build Environment Variables

| Variable | Description |
|----------|-------------|
| `ACCELERATOR` | Hardware platform: `cuda` (default), `metax`, `ascend`, `tsingmicro`, `dcu`, `gcu`, or `musa` |
| `ACCELERATOR` | Hardware platform: `cuda` (default), `metax`, `ascend`, `tsingmicro`, `dcu`, `gcu`, `musa`, or `bpu` |
| `CUDA_HOME` | CUDA toolkit path |
| `DTK_ROOT` | Hygon DTK path (falls back to `ROCM_PATH`, then `/opt/dtk`; required for DCU build) |
| `TOPS_HOME` | Enflame TopsRider SDK path (default `/opt/tops`; required for GCU build) |
Expand All @@ -629,6 +680,9 @@ then rejects.
| `GCU_KERNEL` | Enable Enflame GCU topsaten kernel build (`ON`/`OFF`, auto-enabled when `ACCELERATOR=gcu`) |
| `MUSA_HOME` | Moore Threads MUSA toolkit path (default `/usr/local/musa`; required for MUSA build) |
| `MUSA_KERNEL` | Enable the MUSA mudnn kernel build (`ON`/`OFF`, auto-enabled when `ACCELERATOR=musa`); `OFF` falls back to CPU for all compute |
| `FLAGOS_BPU_X86_PYTHON` | Path to an x86_64 python with `hbdk4-compiler`, run under box64 (BPU compile path; see [docs/bpu.md](docs/bpu.md)) |
| `FLAGOS_BPU_X86_EMULATOR` | Path to a box64 binary (0.4+); the packaged 0.2.6 cannot run on this board's 64 KB pages |
| `FLAGOS_BPU_X86_STUBS` | numba/torch import stubs for the emulated interpreter (defaults next to the x86 python) |

### Runtime Environment Variables

Expand All @@ -644,6 +698,10 @@ then rejects.
| `FLAGOS_LOG_DISPATCH` | Set to `1` to print backend selection for each operator dispatch |
| `FLAGOS_OP_<name>` | Per-operator backend override (replace `.` with `__` in op names) |
| `TORCH_DEVICE_BACKEND_AUTOLOAD` | Set to `0` to stop a vendor plugin (e.g. `torch_musa`) from claiming `PrivateUse1` during `import torch`; `torch_fl` sets this itself on MUSA builds |
| `FLAGOS_BPU_MARCH` | BPU micro-architecture (default `nash-p`) |
| `FLAGOS_BPU_QUANTIZE` | BPU int8 Q/DQ insertion (default `1`; `0` compiles float, which keeps conv on the CPU) |
| `FLAGOS_BPU_ACT_SCALE` | BPU fallback activation scale for uncalibrated tensors (default `0.05`) |
| `FLAGOS_BPU_CACHE` | BPU `.hbm` artifact cache (default `~/.cache/torch_fl_bpu`) |

## Usage

Expand Down
24 changes: 20 additions & 4 deletions csrc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,19 @@ elseif(ACCELERATOR STREQUAL "musa")
# vendor's torch version).
target_link_libraries(${LIBRARY_NAME} PRIVATE ${MUDNN_LIB})
endif()
elseif(ACCELERATOR STREQUAL "bpu")
# Selects backends/bpu_memory.h and, like tsingmicro, skips the generated
# PrivateUse1 op registrations: the BPU executes whole compiled graphs, so
# there is no per-op kernel to register and every aten call reaches
# cpu_fallback. Acceleration comes from the torch.compile backend instead.
target_compile_definitions(${LIBRARY_NAME} PRIVATE USE_BPU=1)
target_link_libraries(${LIBRARY_NAME} PRIVATE ${_torch_fl_link_libs})
find_path(HOBOT_UCP_INCLUDE_DIR hb_ucp_sys.h
PATHS ${HOBOT_INCLUDE_DIR} /usr/include/hobot)
if(HOBOT_UCP_INCLUDE_DIR)
target_include_directories(${LIBRARY_NAME} PRIVATE
${HOBOT_UCP_INCLUDE_DIR} ${HOBOT_UCP_INCLUDE_DIR}/dnn)
endif()
elseif(ACCELERATOR STREQUAL "gcu")
target_compile_definitions(${LIBRARY_NAME} PRIVATE USE_GCU=1)
target_link_libraries(${LIBRARY_NAME} PRIVATE ${_torch_fl_link_libs})
Expand Down Expand Up @@ -343,10 +356,13 @@ install(TARGETS ${LIBRARY_NAME}
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

if(ACCELERATOR STREQUAL "gcu" OR ACCELERATOR STREQUAL "musa")
# torch_fl/__init__.py pins backends_cuda.conf by default, which on GCU/MUSA
# would route every op to the (absent) CUDA kernels. Record the platform next
# to the libs so the Python side can pick backends_<platform>.conf instead.
if(ACCELERATOR STREQUAL "gcu" OR ACCELERATOR STREQUAL "musa"
OR ACCELERATOR STREQUAL "bpu")
# torch_fl/__init__.py pins backends_cuda.conf by default, which on
# GCU/MUSA/BPU would route every op to the (absent) CUDA kernels. Record the
# platform next to the libs so the Python side can pick
# backends_<platform>.conf instead. BPU additionally uses the marker to decide
# whether to register the "bpu" torch.compile backend.
set(_flagos_platform_marker "${CMAKE_CURRENT_BINARY_DIR}/flagos_platform")
file(WRITE "${_flagos_platform_marker}" "${ACCELERATOR}\n")
install(FILES "${_flagos_platform_marker}"
Expand Down
2 changes: 2 additions & 0 deletions csrc/aten/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ std::string DefaultConfigPath() {
platform = "ascend";
#elif defined(USE_MUSA)
platform = "musa";
#elif defined(USE_BPU)
platform = "bpu";
#endif
if (platform) {
// dir is <prefix>/torch_fl/lib, configs are at <prefix>/torch_fl/configs/
Expand Down
4 changes: 2 additions & 2 deletions csrc/aten/contiguous_ops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ at::Tensor contiguous(
// intermediate buffer and no CPU round-trip.
musa_ops::MudnnCopy(self, result);
#elif !defined(USE_ASCEND) && !defined(USE_TSINGMICRO) && !defined(USE_GCU) && \
!defined(USE_MUSA)
!defined(USE_MUSA) && !defined(USE_BPU)
// CUDA platform: use DeviceBoxingGuard to invoke native CUDA strided copy
// kernel on-device, avoiding expensive CPU round-trip.
DeviceBoxingGuard guard(self, result);
Expand Down Expand Up @@ -105,7 +105,7 @@ at::Tensor clone(
// strided copy kernel instead of expensive CPU round-trip.
if (self.is_privateuseone()) {
#if !defined(USE_ASCEND) && !defined(USE_TSINGMICRO) && !defined(USE_GCU) && \
!defined(USE_MUSA)
!defined(USE_MUSA) && !defined(USE_BPU)
auto result = at::empty(
self.sizes(), self.options().memory_format(memory_format));
DeviceBoxingGuard guard(self, result);
Expand Down
13 changes: 7 additions & 6 deletions csrc/aten/copy_ops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
// c10::hip with zero c10::cuda symbols. DCU still shares the vendor's streams,
// so it needs *some* barrier; see SyncCurrentStreamBeforeBlockingCopy below.
#if !defined(USE_ASCEND) && !defined(USE_TSINGMICRO) && !defined(USE_GCU) && \
!defined(USE_MUSA) && !defined(USE_DCU)
!defined(USE_MUSA) && !defined(USE_DCU) && !defined(USE_BPU)
#define FLAGOS_COPY_HAS_CUDA_STREAM 1
#include <c10/cuda/CUDAStream.h>
#endif
Expand Down Expand Up @@ -132,7 +132,7 @@ at::Tensor _copy_from(
// this platform.
musa_ops::MudnnCopy(self, const_cast<at::Tensor&>(dst));
#elif !defined(USE_ASCEND) && !defined(USE_TSINGMICRO) && !defined(USE_GCU) && \
!defined(USE_MUSA)
!defined(USE_MUSA) && !defined(USE_BPU)
// CUDA platform: use DeviceBoxingGuard to dispatch to native CUDA
// strided copy kernel (handles strides, dtype casts on-device).
DeviceBoxingGuard guard(self, dst);
Expand Down Expand Up @@ -214,7 +214,7 @@ at::Tensor _copy_from(
auto tmp = at::empty(self_contig.sizes(), dst.options());
Memcpy(tmp.data_ptr(), self_contig.data_ptr(), nbytes, MemcpyHostToDevice);
#if defined(USE_ASCEND) || defined(USE_TSINGMICRO) || defined(USE_GCU) || \
defined(USE_MUSA)
defined(USE_MUSA) || defined(USE_BPU)
at::native::flagos::_copy_from(tmp, dst, false);
#else
DeviceBoxingGuard guard(tmp, dst);
Expand Down Expand Up @@ -244,7 +244,7 @@ at::Tensor _copy_from(
auto tmp = at::empty(self_contig.sizes(), dst.options());
Memcpy(tmp.data_ptr(), self_contig.data_ptr(), nbytes, MemcpyDeviceToDevice);
#if defined(USE_ASCEND) || defined(USE_TSINGMICRO) || defined(USE_GCU) || \
defined(USE_MUSA)
defined(USE_MUSA) || defined(USE_BPU)
at::native::flagos::_copy_from(tmp, dst, false);
#else
DeviceBoxingGuard guard(tmp, dst);
Expand Down Expand Up @@ -405,7 +405,7 @@ at::Tensor _to_copy(
.dtype(dtype).device(c10::Device(c10::kPrivateUse1, device_index)));
musa_ops::MudnnCopy(self_contig, result);
#elif defined(USE_ASCEND) || defined(USE_TSINGMICRO) || defined(USE_GCU) || \
defined(USE_MUSA)
defined(USE_MUSA) || defined(USE_BPU)
// No CUDA runtime on these backends, so the CUDA TensorIterator cast
// below is unavailable.
#ifdef USE_ASCEND
Expand All @@ -415,7 +415,8 @@ at::Tensor _to_copy(
#endif
if (!result.defined()) {
// Fallback: CPU round-trip when no on-device cast is available
// (TsingMicro / GCU / MUSA, or an Ascend dtype pair aclnnCast rejects).
// (TsingMicro / GCU / MUSA / BPU, or an Ascend dtype pair aclnnCast
// rejects).
size_t nbytes = self_contig.numel() * self_contig.element_size();
at::Tensor cpu_tensor =
at::empty(self_contig.sizes(), self_contig.options().device(at::kCPU));
Expand Down
99 changes: 99 additions & 0 deletions csrc/aten/register.cc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,86 @@ int64_t WrapperFusedSdpChoice(
return static_cast<int64_t>(at::SDPBackend::efficient_attention);
}

#if defined(USE_BPU)
// Convolution on a device with no per-op kernels.
//
// aten::convolution dispatches PrivateUse1 to convolution_overrideable, and the
// only other kernel registered for that op is a CompositeExplicitAutograd stub
// that raises NotImplementedError. So the boxed cpu_fallback cannot help here:
// it moves the arguments to CPU and redispatches the same op, which lands back
// on the stub. These wrappers cross to CPU and then call at::convolution --
// a different op, and the one that actually has a CPU kernel.
at::Tensor BPUWrapperConvolutionOverrideable(
const at::Tensor& input,
const at::Tensor& weight,
const ::std::optional<at::Tensor>& bias,
c10::SymIntArrayRef stride,
c10::SymIntArrayRef padding,
c10::SymIntArrayRef dilation,
bool transposed,
c10::SymIntArrayRef output_padding,
c10::SymInt groups) {
auto out = at::convolution_symint(
input.cpu(),
weight.cpu(),
bias.has_value() && bias->defined()
? ::std::optional<at::Tensor>(bias->cpu())
: ::std::nullopt,
stride,
padding,
dilation,
transposed,
output_padding,
groups);
return out.to(input.device());
}

::std::tuple<at::Tensor, at::Tensor, at::Tensor>
BPUWrapperConvolutionBackwardOverrideable(
const at::Tensor& grad_output,
const at::Tensor& input,
const at::Tensor& weight,
c10::SymIntArrayRef stride,
c10::SymIntArrayRef padding,
c10::SymIntArrayRef dilation,
bool transposed,
c10::SymIntArrayRef output_padding,
c10::SymInt groups,
::std::array<bool, 3> output_mask) {
// convolution_backward wants the forward bias *sizes*, not the bias, and only
// to shape grad_bias -- which is a plain sum over the non-channel dims, so the
// channel count is all it needs.
::std::optional<c10::SymIntArrayRef> bias_sizes = ::std::nullopt;
c10::SymInt out_channels =
transposed ? weight.sym_size(1) * groups : weight.sym_size(0);
::std::vector<c10::SymInt> bias_shape{out_channels};
if (output_mask[2]) {
bias_sizes = c10::SymIntArrayRef(bias_shape);
}

auto out = at::convolution_backward_symint(
grad_output.cpu(),
input.cpu(),
weight.cpu(),
bias_sizes,
stride,
padding,
dilation,
transposed,
output_padding,
groups,
output_mask);

auto to_dev = [&](const at::Tensor& t) {
return t.defined() ? t.to(input.device()) : t;
};
return ::std::make_tuple(
to_dev(::std::get<0>(out)),
to_dev(::std::get<1>(out)),
to_dev(::std::get<2>(out)));
}
#endif // USE_BPU

// ============================================================
// Generated wrappers for 71 CUDA operators
// ============================================================
Expand Down Expand Up @@ -347,6 +427,25 @@ TORCH_LIBRARY_IMPL(aten, PrivateUse1, m) {
#if defined(FLAGOS_MUSA_KERNEL)
#include "backends/musa/generated/musa_register.inc"
#endif
#elif defined(USE_BPU)
// BPU registers no compute ops. The BPU's unit of execution is a whole
// compiled graph (a .hbm produced by hbdk4), so there is no per-operator
// kernel to claim -- and claiming an op on PrivateUse1 without a kernel
// behind it raises "backend not registered" instead of falling back.
// Leaving the list out routes every op to cpu_fallback below; acceleration
// comes from the torch.compile backend in torch_fl/backends/bpu/.
//
// The *_overrideable ops are the exception, and the generic fallback cannot
// serve them. aten::convolution routes PrivateUse1 to
// convolution_overrideable, whose only other kernel is a
// CompositeExplicitAutograd stub that raises NotImplementedError -- so
// cpu_fallback, which moves the arguments to CPU and redispatches the *same*
// op, lands right back on that stub. These wrappers instead call
// at::convolution on the CPU tensors, which is the op that actually has a
// CPU kernel.
m.impl("convolution_overrideable", BPUWrapperConvolutionOverrideable);
m.impl("convolution_backward_overrideable",
BPUWrapperConvolutionBackwardOverrideable);
#else
#define FLAGOS_GEN_IMPLS
#include "generated/register.inc"
Expand Down
Loading
Loading