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
11 changes: 0 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ make build-in-docker
cd dist && sha256sum -c SHA256SUMS
```

This builds the complete release library against real CANN runtime/DCMI libraries
and exports `dist/libvnpu.so`, `dist/build-info.json` and `dist/SHA256SUMS`.
This builds the complete release library, verifies its APIs against real CANN
runtime/DCMI libraries, and exports `dist/libvnpu.so`, `dist/build-info.json` and `dist/SHA256SUMS`.
The Dockerfile uses the official `quay.io/ascend/cann:8.5.2-910b-ubuntu22.04-py3.11`
image, pinned by digest, and Rust **1.94.0**. It downloads the official **HDK 25.5.1**
package, verifies its SHA256 and extracts the real driver user libraries for
linking; it does not install a kernel driver. The build environment is
API verification; it does not install a kernel driver. The build environment is
Comment thread
maverick-woo marked this conversation as resolved.
Resolved
**CANN 8.5.2 on Ubuntu 22.04** and supports **Linux AMD64 and ARM64**. The Make
target selects the host architecture by default. Select a target and separate
output directories explicitly when building both:
Expand All @@ -59,9 +59,8 @@ A successful build does not establish the minimum supported CANN version or
hardware support for every policy. See [testing and CI](docs/testing.md) for
artifact provenance, verification and hardware-validation scope.

To build directly on a Linux host with Rust 1.94.0, a C compiler and real Ascend
CANN/DCMI development libraries, initialize the installed CANN environment and
run `cargo build --release --workspace --locked`. The library is then at
To build directly on a Linux host with Rust 1.94.0 and a C compiler, run
`cargo build --release --workspace --locked`. The library is then at
`target/release/libvnpu.so`. Nonstandard SDK paths are covered in the
[configuration reference](docs/configuration.md).

Expand All @@ -72,7 +71,18 @@ libraries available. Mount a writable host directory at `/hami-shared-region` in
each participating container on the node, and mount the built library at
`/usr/local/lib/libvnpu.so`.

Inside the workload container:
Mount a container-specific file at `/etc/ld.so.preload` with this content:

```text
/usr/local/lib/libvnpu.so
```

Mount this file only into workload containers. The library resolves CANN/runtime
and DCMI on first NPU use, so ordinary commands such as `bash` and `ls` can start
before the CANN environment is prepared. NPU applications still need compatible
runtime/driver libraries. The supervisor and logger also start on first NPU use.

Inside the workload container, launch the framework with its normal command:

```sh
# Shared file for workloads coordinating on physical card 7; use a different file for another card.
Expand All @@ -82,9 +92,12 @@ export NPU_LOCAL_SHM_PATH=/tmp/vnpu-local
export NPU_MEMORY_LIMIT_MB=10240
export NPU_COMPUTE_LIMIT=30
export NPU_SCHED_POLICY=spatial # Default policy; shown explicitly for clarity.
LD_PRELOAD=/usr/local/lib/libvnpu.so python3 your_model.py
python3 your_model.py
```

For local diagnostics, `LD_PRELOAD=/usr/local/lib/libvnpu.so python3 your_model.py`
is also supported; it is not required with the container preload file.

The card number identifies the physical coordination domain; the application
still selects its container-local logical device, commonly `0`.
`NPU_GLOBAL_SHM_PATH` remains required for manager configuration, including
Expand Down
24 changes: 18 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ make build-in-docker
cd dist && sha256sum -c SHA256SUMS
```

该流程使用真实的 CANN runtime/DCMI 库完整构建 release 版本,导出
该流程完整构建 release 版本,并对照真实 CANN runtime/DCMI 库验证 API,导出
`dist/libvnpu.so`、`dist/build-info.json` 和 `dist/SHA256SUMS`。
Dockerfile 使用官方 `quay.io/ascend/cann:8.5.2-910b-ubuntu22.04-py3.11`
镜像并固定摘要,Rust 固定为 **1.94.0**;下载官方 **HDK 25.5.1** 包并验证 SHA256,
只提取真实驱动用户态库用于链接,不安装内核驱动。
只提取真实驱动用户态库用于 API 校验,不安装内核驱动。
构建环境为 **CANN 8.5.2、Ubuntu 22.04**,支持 **Linux AMD64 和 ARM64**,Make
默认选择当前主机架构。构建两种架构时,显式选择目标并使用不同的产物目录:

Expand All @@ -50,8 +50,7 @@ HDK 驱动用户态库版本只说明构建环境,宿主机驱动应与业务
编译成功不能直接确定最低兼容 CANN 版本,也不能代替各策略的真机验证。
产物来源、检查内容和验证范围见[测试与 CI](docs/testing.md)。

如需在已安装真实 CANN/DCMI 开发库的 Linux 主机上直接构建,准备 Rust 1.94.0
和 C 编译器,按安装包说明初始化 CANN 环境后执行
也可在 Linux 主机上直接构建,准备 Rust 1.94.0 和 C 编译器后执行
`cargo build --release --workspace --locked`,产物为 `target/release/libvnpu.so`。
非标准 SDK 路径见[配置参考](docs/configuration.zh-CN.md)。

Expand All @@ -61,7 +60,17 @@ HDK 驱动用户态库版本只说明构建环境,宿主机驱动应与业务
将同一个可写的宿主机共享目录挂载到参与容器的 `/hami-shared-region`,
将构建的库挂载到 `/usr/local/lib/libvnpu.so`。

在业务容器内执行:
将仅用于该业务容器的预加载文件挂载到 `/etc/ld.so.preload`,内容为:

```text
/usr/local/lib/libvnpu.so
```

此文件只挂载到业务容器。库在首次 NPU 使用时按需解析 CANN runtime 和 DCMI,
因此 `bash`、`ls` 等普通命令可以在 CANN 环境准备好之前启动;NPU 应用仍需提供
兼容的 runtime/driver 库。supervisor 和 logger 也在首次 NPU 使用时启动。

在业务容器内使用框架原有命令启动:

```sh
# 同一物理卡 7 上需要协调的负载共享此文件,其他物理卡使用不同文件。
Expand All @@ -71,9 +80,12 @@ export NPU_LOCAL_SHM_PATH=/tmp/vnpu-local
export NPU_MEMORY_LIMIT_MB=10240
export NPU_COMPUTE_LIMIT=30
export NPU_SCHED_POLICY=spatial # 默认策略,此处显式写出。
LD_PRELOAD=/usr/local/lib/libvnpu.so python3 your_model.py
python3 your_model.py
```

本地排障也可以使用 `LD_PRELOAD=/usr/local/lib/libvnpu.so python3 your_model.py`;
使用容器预加载文件时无需设置该变量。

文件名中的卡号表示物理设备协调范围;业务仍选择容器内的逻辑设备,通常为 `0`。
包括默认空分策略在内,manager 配置仍要求设置 `NPU_GLOBAL_SHM_PATH`。

Expand Down
27 changes: 9 additions & 18 deletions crates/hook/src/host_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::cell::Cell;
use std::ffi::c_void;
use std::sync::LazyLock;
use std::sync::atomic::AtomicPtr;

thread_local! {
// PID rather than a boolean: a fork child must not inherit an admission bypass.
Expand Down Expand Up @@ -51,25 +51,16 @@ type HostArgsFn = unsafe extern "C" fn(
usize,
) -> i32;

static REAL: LazyLock<HostArgsFn> = LazyLock::new(|| unsafe {
let name = c"aclrtLaunchKernelWithHostArgs";
let mut symbol = libc::dlsym(libc::RTLD_NEXT, name.as_ptr());
if symbol.is_null() {
// Frameworks can load ACL with RTLD_LOCAL. Resolve its public entry point
// explicitly in that case and retain the handle for the process lifetime.
let handle = libc::dlopen(
c"libascendcl.so".as_ptr(),
libc::RTLD_NOW | libc::RTLD_LOCAL,
);
assert!(!handle.is_null(), "cannot load CANN libascendcl.so");
symbol = libc::dlsym(handle, name.as_ptr());
}
fn real() -> HostArgsFn {
static REAL: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
let symbol = limiter::dynamic::resolve_sdk_symbol(&REAL, c"aclrtLaunchKernelWithHostArgs")
.expect("cannot resolve CANN aclrtLaunchKernelWithHostArgs");
assert!(
!symbol.is_null() && symbol != aclrtLaunchKernelWithHostArgs as *const () as *mut c_void,
symbol != aclrtLaunchKernelWithHostArgs as *const () as *mut c_void,
"cannot resolve CANN aclrtLaunchKernelWithHostArgs"
);
std::mem::transmute(symbol)
});
unsafe { std::mem::transmute(symbol) }
}

// CANN acl_rt.h ABI: aclError is a signed C int; argument sizes/counts are size_t.
// Opaque pointers are forwarded without inspecting their pointed-to layouts.
Expand All @@ -85,7 +76,7 @@ pub unsafe extern "C" fn aclrtLaunchKernelWithHostArgs(
placeholders: *mut c_void,
placeholder_count: usize,
) -> i32 {
let real = *REAL;
let real = real();
let scope = HostArgsScope::enter();
if scope.outermost {
let mut capture_status = 0;
Expand Down
38 changes: 27 additions & 11 deletions crates/hook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct Clients {
// PID-aware storage avoids inherited locks after fork. Each physical card owns
// its own quota, manager election, timing contexts and allocation handles.
static CLIENTS: process::ProcessLocal<Clients> = process::ProcessLocal::new();
static STUB: process::ProcessLocal<SchedulerClient> = process::ProcessLocal::new();

fn fatal(reason: &str) -> ! {
eprintln!("[hami-vnpu-core] {reason}");
Expand Down Expand Up @@ -83,6 +84,9 @@ fn ensure_supervisor(entry: &DeviceClient, physical: u32) {
}

pub fn npu_limiter() -> &'static SchedulerClient {
if !limiter::config::vnpu_configuration_requested() {
return STUB.get_or_init(std::process::id(), SchedulerClient::stub);
}
// Apply spatial budgets before resource creation or operator submission.
spatial::ensure_current_device();
client_for_physical(current_physical_device())
Expand All @@ -91,6 +95,9 @@ pub fn npu_limiter() -> &'static SchedulerClient {
/// Physical-memory properties can target a card other than the current card.
/// Resolve the target without changing the application's current context.
pub fn npu_limiter_for_device(logical: i32) -> &'static SchedulerClient {
if !limiter::config::vnpu_configuration_requested() {
return STUB.get_or_init(std::process::id(), SchedulerClient::stub);
}
// Physical memory can target a device before it has a compute context.
// Apply core budgets when the application selects that context or launches.
if spatial::enabled() {
Expand Down Expand Up @@ -163,19 +170,29 @@ pub(crate) fn retire_context(context: u64) {
}
}

fn missing_runtime_symbol(name: &str) -> u64 {
if limiter::config::vnpu_configuration_requested() {
fatal(&format!("cannot find original function: {name}"));
}
log::debug!("optional Ascend runtime symbol `{name}` is unavailable");
u64::MAX
}

macro_rules! passthrough {
($name:expr, ($($sig:tt)*), $($arg:expr),*) => {
{
static REAL: ::std::sync::LazyLock<extern "C" fn($($sig)*) -> u64> =
::std::sync::LazyLock::new(|| unsafe {
let ptr = libc::dlsym(libc::RTLD_NEXT, concat!($name, "\0").as_ptr() as *const libc::c_char);
if ptr.is_null() {
panic!("cannot find original function: {}", $name);
}
std::mem::transmute(ptr)
});
// println!("in func {:?}", $name);
(*REAL)($($arg),*)
type Real = unsafe extern "C" fn($($sig)*) -> u64;
static REAL: ::std::sync::atomic::AtomicPtr<libc::c_void> =
::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
let name = ::std::ffi::CStr::from_bytes_with_nul(concat!($name, "\0").as_bytes())
.expect("static hook symbol");
match limiter::dynamic::resolve_sdk_symbol(&REAL, name) {
Some(address) => {
let real: Real = unsafe { ::std::mem::transmute(address) };
unsafe { real($($arg),*) }
}
None => crate::missing_runtime_symbol($name),
}
}
};
}
Expand All @@ -187,5 +204,4 @@ mod memory_extra;
mod memory_physical;
mod ownership;
mod process;
mod signal_compat;
pub mod spatial;
6 changes: 2 additions & 4 deletions crates/hook/src/ownership.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,8 @@ fn records() -> MutexGuard<'static, Records> {
}

unsafe fn symbol<T: Copy>(name: &CStr) -> T {
let address = unsafe { libc::dlsym(libc::RTLD_NEXT, name.as_ptr()) };
if address.is_null() {
crate::spatial::fatal(name.to_str().unwrap_or("missing CANN lifecycle API"));
}
let address = limiter::dynamic::sdk_symbol(name)
.unwrap_or_else(|| crate::spatial::fatal(name.to_str().unwrap_or("missing CANN symbol")));
assert_eq!(std::mem::size_of::<T>(), std::mem::size_of_val(&address));
unsafe { std::mem::transmute_copy(&address) }
}
Expand Down
32 changes: 0 additions & 32 deletions crates/hook/src/signal_compat.rs

This file was deleted.

10 changes: 4 additions & 6 deletions crates/hook/src/spatial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ fn policy() -> &'static Policy {
let pid = std::process::id();
let policy = POLICY.get_or_init(pid, || {
// Other policies must not require any of the optional resource APIs.
let spatial =
SchedPolicy::from_env().unwrap_or_else(|error| fatal(&error)) == SchedPolicy::Spatial;
let spatial = limiter::config::vnpu_configuration_requested()
&& SchedPolicy::from_env().unwrap_or_else(|error| fatal(&error))
== SchedPolicy::Spatial;
let percent = if spatial {
let cfg = ManagerConfig::from_env().unwrap_or_else(|error| fatal(&error));
if cfg.sched_policy != SchedPolicy::Spatial {
Expand Down Expand Up @@ -127,10 +128,7 @@ unsafe fn symbol<T: Copy>(name: &CStr) -> T {
}

unsafe fn optional_symbol<T: Copy>(name: &CStr) -> Option<T> {
let address = unsafe { libc::dlsym(libc::RTLD_NEXT, name.as_ptr()) };
if address.is_null() {
return None;
}
let address = limiter::dynamic::sdk_symbol(name)?;
// Each call site supplies the exact function-pointer type from acl_rt.h.
assert_eq!(std::mem::size_of::<T>(), std::mem::size_of_val(&address));
Some(unsafe { std::mem::transmute_copy(&address) })
Expand Down
1 change: 0 additions & 1 deletion crates/limiter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ edition.workspace = true
libc = { workspace = true }
log = "0.4"
env_logger = "0.11"
ctor = "0.2"
13 changes: 2 additions & 11 deletions crates/limiter/build.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
// Link the Ascend DCMI (+ CANN runtime) libs. Paths default to the standard Ascend
// install and can be overridden via env (CI, a relocated SDK, etc.) rather than hardcoded.
fn main() {
let toolkit = std::env::var("ASCEND_TOOLKIT_HOME")
.unwrap_or_else(|_| "/usr/local/Ascend/ascend-toolkit/latest".into());
let driver =
std::env::var("ASCEND_DRIVER_HOME").unwrap_or_else(|_| "/usr/local/Ascend/driver".into());
println!("cargo:rustc-link-search=native={toolkit}/lib64");
println!("cargo:rustc-link-search=native={driver}/lib64/driver");
println!("cargo:rustc-link-lib=dcmi");
println!("cargo:rerun-if-env-changed=ASCEND_TOOLKIT_HOME");
println!("cargo:rerun-if-env-changed=ASCEND_DRIVER_HOME");
// A system-preloaded libvnpu must also load in ordinary shells without CANN.
// Resolve runtime/DCMI entry points lazily instead of adding DT_NEEDED entries.
}
17 changes: 12 additions & 5 deletions crates/limiter/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@ impl ManagerConfig {
}
}

/// Validate explicit resource settings before a worker can enter the stub path.
pub fn validate_requested_limits() -> Result<(), String> {
let requested = [
/// System preload alone does not opt an unrelated process into NPU control.
/// Explicit settings, including invalid or legacy values, must still fail closed.
pub fn vnpu_configuration_requested() -> bool {
[
ENV_GLOBAL_SHM_PATH,
ENV_LOCAL_SHM_PATH,
ENV_MEMORY_LIMIT_MB,
ENV_COMPUTE_LIMIT,
ENV_SCHED_POLICY,
Expand All @@ -225,8 +228,12 @@ pub fn validate_requested_limits() -> Result<(), String> {
"NPU_OVERDRAFT_ENABLED",
]
.iter()
.any(|name| std::env::var_os(name).is_some());
if requested {
.any(|name| std::env::var_os(name).is_some())
}

/// Validate explicit resource settings before a worker can enter the stub path.
pub fn validate_requested_limits() -> Result<(), String> {
if vnpu_configuration_requested() {
ManagerConfig::from_env().map(|_| ())
} else {
Ok(())
Expand Down
Loading
Loading