Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7a18d31
feat(hardware_backend): add native CUDA ABI type aliases
khwstolle Jul 27, 2026
a69e651
fix(hardware_backend): remove em-dashes from ABI doc comments
khwstolle Jul 27, 2026
6eff6fd
fix(hardware_backend): remove remaining em-dash from ABI doc comment
khwstolle Jul 27, 2026
4bf7d43
feat(hardware_backend): required native symbol probe, replacing lenie…
khwstolle Jul 27, 2026
5462000
feat(hardware_backend): invoke_hardware_backend_* methods on NativeEx…
khwstolle Jul 27, 2026
889cf77
feat(hardware_backend): DispatchedHardwareBackendCuda trait + native …
khwstolle Jul 27, 2026
55ffdb3
feat(hardware_backend): HardwareBackendRouter, native-only, capabilit…
khwstolle Jul 27, 2026
32348ab
fix(hardware_backend): require hardware_class capability be declared,…
khwstolle Jul 27, 2026
7ff679c
feat(hardware_backend): re-export HardwareBackendRouter from flavor m…
khwstolle Jul 28, 2026
7d87e67
feat(hardware_backend): PyO3 binding with hand-rolled DLPack capsule …
khwstolle Jul 28, 2026
80ad145
test(hardware_backend): capability-policy coverage for a realistic CU…
khwstolle Jul 28, 2026
ee4922f
feat(hardware_backend): correct ext-init scaffold (was silently falli…
khwstolle Jul 28, 2026
61c9b39
feat(hardware_backend): add native-flavor CPU-passthrough export to r…
khwstolle Jul 28, 2026
913f1e0
fix(hardware_backend): gate unused Layout import to wasm32-only build
khwstolle Jul 28, 2026
ed21819
feat(hardware_backend): additive HardwareBackendRouter::new_with_policy
khwstolle Jul 28, 2026
927d939
feat(hardware_backend): wire a caller-supplied policy into the PyO3 b…
khwstolle Jul 28, 2026
116772f
test(hardware_backend): real dlopen round-trip against the compiled r…
khwstolle Jul 28, 2026
ca1ddb0
test(hardware_backend): Python interop round-trip via the real ext-bu…
khwstolle Jul 28, 2026
0786e1f
test(hardware_backend): concurrent first-access router resolution
khwstolle Jul 28, 2026
32cf737
ci(hardware_backend): build and test the native reference fixture on …
khwstolle Jul 28, 2026
9d64d08
feat(ext-tooling): branch native builds to cargo-oxide when the crate…
khwstolle Jul 28, 2026
8cc9b15
style: format Rust code with cargo fmt and Python code with ruff format
khwstolle Jul 29, 2026
8982a42
fix(clippy): fix unused import and allow too_many_arguments on Hardwa…
khwstolle Jul 29, 2026
f9df747
fix(clippy): resolve clippy warnings across tests and codecs
khwstolle Jul 29, 2026
3f3aaf9
fix(clippy): resolve clippy 1.97 lints in wasm, huff_llm, and base64
khwstolle Jul 29, 2026
caa171c
style: format wasm.rs with cargo fmt
khwstolle Jul 29, 2026
c9ca35a
fix(clippy): use is_multiple_of(8) in ptwm-py hardware binding
khwstolle Jul 29, 2026
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: 11 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,16 @@ jobs:
- name: Install ext-dev deps (for the wasmtime Python package)
run: uv pip install --system 'wasmtime>=26' 'zstandard>=0.22' 'tomli-w>=1.0.0'

- name: Build ref_hardware_backend (native flavor)
run: |
cd extensions/ref_hardware_backend/rust
cargo build --release

- name: Run hardware_backend native dispatch tests
run: cargo test -p ptwm-core --test hardware_backend_router_native -- --ignored

- name: Run interop tests
run: uv run pytest tests/interop -v --no-cov

- name: Run hardware_backend dispatch pytest
run: uv run pytest tests/ptwm/ext/test_hardware_backend_dispatch.py -v --no-cov
4 changes: 2 additions & 2 deletions crates/ptwm-core/src/codecs/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,8 @@ mod tests {
let data: Vec<u8> = (0..262144)
.map(|i| match i % 16 {
0..=9 => 0x40u8,
10 | 11 | 12 => 0x41,
13 | 14 => 0x3F,
10..=12 => 0x41,
13..=14 => 0x3F,
_ => 0x42,
})
.collect();
Expand Down
4 changes: 2 additions & 2 deletions crates/ptwm-core/src/codecs/huff_llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl PlaneCodec for HuffLlm5Bit {
_layout: &PlaneLayout,
) -> bool {
// Skip tiny planes (three table headers don't amortize) and odd lengths.
plane.len() >= MIN_WORDS * 2 && plane.len() % 2 == 0
plane.len() >= MIN_WORDS * 2 && plane.len().is_multiple_of(2)
}

fn encode(
Expand All @@ -82,7 +82,7 @@ impl PlaneCodec for HuffLlm5Bit {
// Empty or odd-length → raw store (still total). Short-circuit empty
// before allocating `words`, since trial-encode hits this path on every
// candidate.
if plane.is_empty() || plane.len() % 2 != 0 {
if plane.is_empty() || !plane.len().is_multiple_of(2) {
return Ok(raw_store(plane));
}
let words: Vec<u16> = plane
Expand Down
2 changes: 1 addition & 1 deletion crates/ptwm-core/src/entropy/context_mixing/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ mod tests {
for &byte in data {
let mut c0: u32 = 1;
for i in (0..8).rev() {
let bit = ((byte >> i) & 1) as u8;
let bit = (byte >> i) & 1;
let preds = models.predict(c0);
out.push(preds[0]);
models.update(c0, bit);
Expand Down
6 changes: 4 additions & 2 deletions crates/ptwm-core/src/extension/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ impl CanonicalId {
}
let mut out = [0u8; 32];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0]).ok_or_else(|| super::ExtensionError::InvalidRef(s.to_string()))?;
let lo = hex_nibble(chunk[1]).ok_or_else(|| super::ExtensionError::InvalidRef(s.to_string()))?;
let hi = hex_nibble(chunk[0])
.ok_or_else(|| super::ExtensionError::InvalidRef(s.to_string()))?;
let lo = hex_nibble(chunk[1])
.ok_or_else(|| super::ExtensionError::InvalidRef(s.to_string()))?;
out[i] = (hi << 4) | lo;
}
Ok(Self(out))
Expand Down
3 changes: 2 additions & 1 deletion crates/ptwm-core/src/flavor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ pub use native::{
DeltaSchemeFn, NativeExtension, NativeSymbols, PlaneCodecFn, TransformFn, VerifiedToken,
};
pub use router::{
DeltaSchemeRouter, DispatchedDeltaScheme, DispatchedPlaneCodec, PlaneCodecRouter,
DeltaSchemeRouter, DispatchedDeltaScheme, DispatchedHardwareBackendCuda, DispatchedPlaneCodec,
HardwareBackendRouter, PlaneCodecRouter,
};
pub use third_party::ThirdPartyPlaneCodec;
pub use wasm::{
Expand Down
174 changes: 174 additions & 0 deletions crates/ptwm-core/src/flavor/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub struct NativeSymbols {
// DeltaScheme
pub delta_scheme_v1_encode: Option<DeltaSchemeFn>,
pub delta_scheme_v1_decode: Option<DeltaSchemeFn>,
// HardwareBackend
pub hardware_backend_v1_cuda_stream_handle: Option<HardwareBackendCudaStreamHandleFn>,
pub hardware_backend_v1_dispatch_decode_cuda: Option<HardwareBackendCudaDispatchDecodeFn>,
}

pub type PlaneCodecFn = unsafe extern "C" fn(
Expand Down Expand Up @@ -94,6 +97,44 @@ pub type DeltaSchemeFn = unsafe extern "C" fn(
usize, // output len
) -> i64;

/// `hardware_backend_v1_cuda_stream_handle`: returns this backend's own,
/// process-persistent CUDA stream (as a raw pointer value) for the given
/// device ordinal. Callers pass this same handle to `tensor.__dlpack__
/// (stream=...)` for every tensor involved in a subsequent decode call, so
/// PyTorch's DLPack producer inserts the correct cross-stream wait before
/// handing back the device pointer. Returns 0 on failure (no such device,
/// CUDA init failed).
pub type HardwareBackendCudaStreamHandleFn = unsafe extern "C" fn(device_ordinal: u32) -> u64;

/// `hardware_backend_v1_dispatch_decode_cuda`: decode `in_dev_ptr` into
/// `out_dev_ptr`, two distinct device buffers (this is NOT an in-place
/// transform over one buffer; "zero-copy" means no host round-trip, not a
/// shared address). `state_bytes` carries the codec's small per-tensor
/// state (e.g. a 16-entry codebook), in the same `(state_format_version,
/// state_bytes)` shape `plane_codec`'s existing `decode_stateful` path
/// already carries. `codec_id` self-describes the wire format for the
/// extension to validate against (distinct from the backend's own
/// canonical id, which the router already resolved to get here).
///
/// Completion contract: by the time this function returns, the kernel has
/// FULLY COMPLETED (the extension synchronizes its own stream before
/// returning). `out_dev_ptr`'s contents are valid and visible to any
/// subsequent CUDA operation on any stream, with no further caller-side
/// synchronization needed. Both launch-time and execution-time errors are
/// visible via the return code, since the synchronize() call observes both.
pub type HardwareBackendCudaDispatchDecodeFn = unsafe extern "C" fn(
state_format_version: u8,
state_ptr: *const u8,
state_len: usize,
codec_id_ptr: *const u8,
codec_id_len: usize,
in_dev_ptr: u64,
in_len: usize,
out_dev_ptr: u64,
out_len: usize,
device_ordinal: u32,
) -> i64;

pub struct NativeExtension {
// Held for its drop-time side effect: keeping the dlopen handle alive
// so all fn pointers in `symbols` remain valid.
Expand Down Expand Up @@ -126,6 +167,8 @@ impl NativeExtension {
transform_v1_inverse: None,
delta_scheme_v1_encode: None,
delta_scheme_v1_decode: None,
hardware_backend_v1_cuda_stream_handle: None,
hardware_backend_v1_dispatch_decode_cuda: None,
};

// Probe symbols depending on the kind.
Expand Down Expand Up @@ -175,6 +218,25 @@ impl NativeExtension {
});
}
}
Kind::HardwareBackend => {
symbols.hardware_backend_v1_cuda_stream_handle =
resolve::<HardwareBackendCudaStreamHandleFn>(
&library,
b"ptwm_hardware_backend_v1_cuda_stream_handle\0",
);
symbols.hardware_backend_v1_dispatch_decode_cuda =
resolve::<HardwareBackendCudaDispatchDecodeFn>(
&library,
b"ptwm_hardware_backend_v1_dispatch_decode_cuda\0",
);
if symbols.hardware_backend_v1_cuda_stream_handle.is_none()
|| symbols.hardware_backend_v1_dispatch_decode_cuda.is_none()
{
return Err(CodecError::Unsupported {
feature: "missing required hardware_backend_v1 symbol".into(),
});
}
}
_ => {
// Other kinds: probe is lenient for v1. The dispatcher
// will yield Unsupported on invocation if the symbol's
Expand Down Expand Up @@ -335,6 +397,64 @@ impl NativeExtension {
};
decode_rc(rc, output.len())
}

/// Invoke `ptwm_hardware_backend_v1_cuda_stream_handle`. Returns this
/// backend's process-persistent CUDA stream handle for `device_ordinal`.
pub fn invoke_hardware_backend_cuda_stream_handle(
&self,
device_ordinal: u32,
) -> Result<u64, CodecError> {
let f =
self.symbols
.hardware_backend_v1_cuda_stream_handle
.ok_or(CodecError::Unsupported {
feature: "hardware_backend_v1_cuda_stream_handle not present".into(),
})?;
let handle = unsafe { f(device_ordinal) };
if handle == 0 {
return Err(CodecError::Unsupported {
feature: "hardware backend failed to produce a CUDA stream handle".into(),
});
}
Ok(handle)
}

/// Invoke `ptwm_hardware_backend_v1_dispatch_decode_cuda`. Decodes
/// `in_dev_ptr` into `out_dev_ptr`, two distinct device buffers.
#[allow(clippy::too_many_arguments)]
pub fn invoke_hardware_backend_dispatch_decode_cuda(
&self,
state_bytes: &[u8],
codec_id: &CanonicalId,
in_dev_ptr: u64,
in_len: usize,
out_dev_ptr: u64,
out_len: usize,
device_ordinal: u32,
) -> Result<usize, CodecError> {
let f = self
.symbols
.hardware_backend_v1_dispatch_decode_cuda
.ok_or(CodecError::Unsupported {
feature: "hardware_backend_v1_dispatch_decode_cuda not present".into(),
})?;
let codec_id_bytes = codec_id.as_bytes();
let rc = unsafe {
f(
1, // state_format_version
state_bytes.as_ptr(),
state_bytes.len(),
codec_id_bytes.as_ptr(),
codec_id_bytes.len(),
in_dev_ptr,
in_len,
out_dev_ptr,
out_len,
device_ordinal,
)
};
decode_rc(rc, out_len)
}
}

fn decode_rc(rc: i64, out_capacity: usize) -> Result<usize, CodecError> {
Expand Down Expand Up @@ -389,6 +509,35 @@ mod tests {
}
}

/// Build a bare `NativeExtension` with every `NativeSymbols` field set
/// to `None`, for exercising "symbol absent" invoke paths without
/// dlopen-ing a real contribution artifact from disk.
///
/// `Library::this()` wraps a handle to the already-loaded host process
/// instead of opening a new shared object, which gives a real, valid
/// `Library` (fn pointers can be safely dropped/never resolved against
/// it) without depending on any particular file existing on disk.
fn native_extension_with_no_hardware_backend_symbols() -> NativeExtension {
let library: Library = libloading::os::unix::Library::this().into();
NativeExtension {
library,
canonical_id: CanonicalId::from_bytes([0xCD; 32]),
kind: Kind::HardwareBackend,
lifecycle: Lifecycle::Thread,
symbols: NativeSymbols {
plane_codec_v1_encode: None,
plane_codec_v1_decode: None,
plane_codec_v1_decode_stateful: None,
transform_v1_forward: None,
transform_v1_inverse: None,
delta_scheme_v1_encode: None,
delta_scheme_v1_decode: None,
hardware_backend_v1_cuda_stream_handle: None,
hardware_backend_v1_dispatch_decode_cuda: None,
},
}
}

#[test]
fn missing_library_path_is_invalid_input() {
let res = NativeExtension::load(
Expand All @@ -398,4 +547,29 @@ mod tests {
);
assert!(matches!(res, Err(CodecError::InvalidInput)));
}

#[test]
fn hardware_backend_invoke_stream_handle_errors_when_symbol_absent() {
// A NativeExtension whose hardware_backend_v1_cuda_stream_handle symbol
// was never resolved (None) must return Unsupported, not panic/UB.
let ext = native_extension_with_no_hardware_backend_symbols(); // test-only constructor, see Step 3
let res = ext.invoke_hardware_backend_cuda_stream_handle(0);
assert!(matches!(res, Err(CodecError::Unsupported { .. })));
}

#[test]
fn hardware_backend_missing_required_symbols_is_unsupported() {
// A library with neither ptwm_hardware_backend_v1_cuda_stream_handle
// nor ptwm_hardware_backend_v1_dispatch_decode_cuda must fail to load
// for Kind::HardwareBackend, same strictness as PlaneCodec/DeltaScheme.
// This test is a placeholder for Task 11's real artifact-backed test;
// the path-doesn't-exist error fires before symbol probing, so the test
// passes trivially for now.
let res = NativeExtension::load(
Path::new("/this/path/does/not/exist.so"),
&entry(Kind::HardwareBackend),
VerifiedToken::new_unchecked(),
);
assert!(matches!(res, Err(CodecError::InvalidInput)));
}
}
Loading
Loading