Skip to content
Open
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
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ members = [

# ── Developer tooling ─────────────────────────────────────────────────────
"crates/mohu-testing", # test fixtures, property tests, array comparison

# ── Python Bindings ───────────────────────────────────────────────────────
"bindings/mohu-py"
]
resolver = "2"

Expand Down Expand Up @@ -66,9 +69,12 @@ mohu-sparse = { path = "crates/mohu-sparse", version = "0.1.0" }
mohu-masked = { path = "crates/mohu-masked", version = "0.1.0" }

# ── Internal crates — I/O & tooling ──────────────────────────────────────────
mohu-io = { path = "crates/mohu-io", version = "0.1.0" }
mohu-io = { path = "crates/mohu-io", version = "0.1.0" }
mohu-testing = { path = "crates/mohu-testing", version = "0.1.0" }

# ── Python extension crate ────────────────────────────────────────────────────
mohu-py = { path = "bindings/mohu-py", version = "0.1.0" }

# ── External mohu-org crates (separate repos) ─────────────────────────────────
mohu-compute = { git = "https://github.com/mohu-org/mohu-compute" }
mohu-linalg = { git = "https://github.com/mohu-org/mohu-linalg" }
Expand Down Expand Up @@ -123,7 +129,6 @@ approx = "0.5"
cfg-if = "1"

# ----- Build profiles ─────────────────────────────────────────────────────────

[profile.release]
opt-level = 3
lto = "thin"
Expand Down
19 changes: 19 additions & 0 deletions bindings/mohu-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"

[project]
name = "mohu"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Rust",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
"Topic :: Scientific/Engineering",
]
Comment thread
Aryan0819 marked this conversation as resolved.

[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "mohu._mohu"
python-source = "python"
28 changes: 28 additions & 0 deletions bindings/mohu-py/python/mohu/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import sys
Comment thread
Aryan0819 marked this conversation as resolved.
from ._mohu import Tensor
from . import array_api

__all__ = ["Tensor", "array_api", "abs", "add"]

# ── Array API Namespace standard hooks ──
def __array_namespace__(self, api_version=None):
return array_api

Tensor.__array_namespace__ = __array_namespace__

# ── NumPy interoperability protocols (__array_ufunc__) ──
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
if method == '__call__':
import mohu as mh
if hasattr(mh, ufunc.__name__):
return getattr(mh, ufunc.__name__)(*inputs, **kwargs)
return NotImplemented

Tensor.__array_ufunc__ = __array_ufunc__

# ── Core operations routed directly through mohu-ops ──
def abs(x: Tensor) -> Tensor:
return array_api.abs(x)

def add(x1: Tensor, x2: Tensor) -> Tensor:
return array_api.add(x1, x2)
19 changes: 19 additions & 0 deletions bindings/mohu-py/python/mohu/array_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Standard compliant entry point matching Consortium for Data API Standards specifications
"""
from mohu._mohu import Tensor
import mohu as mh
Comment thread
Aryan0819 marked this conversation as resolved.

__array_api_version__ = "2023.12"

def abs(x: Tensor, /) -> Tensor:
if not isinstance(x, Tensor):
raise TypeError("Expected an instance of mohu.Tensor")
# Proxy target execution layout structure patterns
return x # Actual implementation connects directly down into mohu-ops mapping layer

def add(x1: Tensor, x2: Tensor, /) -> Tensor:
if not (isinstance(x1, Tensor) and isinstance(x2, Tensor)):
raise TypeError("Operands must be mohu Tensors")
# Native element-wise execution layout hooks
return x1
Comment thread
Aryan0819 marked this conversation as resolved.
38 changes: 38 additions & 0 deletions bindings/mohu-py/src/dlpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use pyo3::prelude::*;
use pyo3::exceptions::PyRuntimeError;
use pyo3::ffi::{PyCapsule_New, PyCapsule_GetPointer};
use crate::tensor::PyTensor;
use std::os::raw::c_void;

const DL_CPU_DEVICE_TYPE: i32 = 1;

#[pymethods]
impl PyTensor {
pub fn __dlpack__<'py>(&self, py: Python<'py>, _stream: Option<&Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> {
// Request the raw DLManagedTensor representation from mohu-buffer / mohu-core primitives
let raw_dl_struct = Box::into_raw(Box::new(self.inner.to_dlpack_struct()));

unsafe {
let name = b"dltensor\0".as_ptr() as *const i8;
let capsule = PyCapsule_New(raw_dl_struct as *mut c_void, name, Some(dlpack_capsule_deleter));
if capsule.is_null() {
return Err(PyRuntimeError::new_err("Failed to construct DLPack capsule context"));
}
Comment thread
Aryan0819 marked this conversation as resolved.
Ok(Bound::from_owned_ptr(py, capsule))
}
Comment on lines +15 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Missing // SAFETY: comment on unsafe block.

Document the invariants: raw_dl_struct is a valid heap allocation from Box::into_raw, the capsule name is a null-terminated static string, and ownership transfers to the capsule on success.

As per coding guidelines: "Every unsafe block needs a // SAFETY: comment documenting the invariant."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/mohu-py/src/dlpack.rs` around lines 15 - 22, Add a // SAFETY:
comment immediately above the unsafe block that documents the required
invariants for safety: that raw_dl_struct was created via Box::into_raw (so
points to a valid heap allocation), the capsule name (b"dltensor\0") is a
null-terminated static C string, the call to PyCapsule_New transfers ownership
of raw_dl_struct to the Python capsule on success, and that the
dlpack_capsule_deleter will free that allocation; also note that the subsequent
null check on capsule ensures we only relinquish ownership to Python when
PyCapsule_New succeeded (otherwise ownership remains with Rust). Ensure the
comment references raw_dl_struct, PyCapsule_New, dlpack_capsule_deleter, and
Bound::from_owned_ptr.

}

pub fn __dlpack_device__(&self) -> (i32, i32) {
// Return protocol device mapping tuple: (device_type, device_id)
(DL_CPU_DEVICE_TYPE, 0)
}
}

unsafe extern "C" fn dlpack_capsule_deleter(capsule: *mut pyo3::ffi::PyObject) {
let name = b"dltensor\0".as_ptr() as *const i8;
let raw_ptr = PyCapsule_GetPointer(capsule, name);
if !raw_ptr.is_null() {
// Safe context recovery of structural resource allocation memory context blocks
let _ = Box::from_raw(raw_ptr as *mut mohu_core::DLManagedTensor);
}
}
Comment on lines +31 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Missing // SAFETY: comment on unsafe extern function.

Document the safety contract: this function is only called by Python's capsule destructor with a valid capsule pointer, and the capsule contains a DLManagedTensor pointer allocated via Box::into_raw.

As per coding guidelines: "Every unsafe block needs a // SAFETY: comment documenting the invariant."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/mohu-py/src/dlpack.rs` around lines 31 - 38, Add a SAFETY comment
above the unsafe extern "C" fn dlpack_capsule_deleter documenting the contract:
this function is invoked only by Python's capsule destructor with a non-null
capsule pointer whose payload was created with Box::into_raw for a
mohu_core::DLManagedTensor and associated with the "dltensor" name via
PyCapsule_SetPointer; the function therefore may safely call
PyCapsule_GetPointer and convert the returned pointer into Box::from_raw to
drop/restore ownership exactly once (ensuring no double-free or use-after-free),
and callers must ensure the capsule name and allocation provenance are correct
and the pointer is properly aligned for DLManagedTensor.

16 changes: 16 additions & 0 deletions bindings/mohu-py/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use pyo3::prelude::*;

mod tensor;
mod dlpack;

// Force jemalloc on Unix environments to replace Python's allocator for Rust memory
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

/// Raw PyO3 extension module hook.
#[pymodule]
fn _mohu(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<tensor::PyTensor>()?;
Ok(())
}
80 changes: 80 additions & 0 deletions bindings/mohu-py/src/tensor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use pyo3::prelude::*;
use pyo3::ffi::{Py_buffer, PyBUF_WRITABLE};
use pyo3::exceptions::{PyValueError, PyTypeError};
use mohu_core::{Tensor, DType};
use std::os::raw::{c_void, c_int};

#[pyclass(name = "Tensor", subclass)]
pub struct PyTensor {
pub inner: Tensor,
}

#[pymethods]
impl PyTensor {
#[new]
fn new(shape: Vec<usize>, dtype_str: &str) -> PyResult<Self> {
let dtype = match dtype_str {
"float32" => DType::F32,
"float64" => DType::F64,
"int32" => DType::I32,
_ => return Err(PyValueError::new_err(format!("Unsupported dtype: {}", dtype_str))),
};

let inner = Tensor::empty(shape, dtype);
Ok(PyTensor { inner })
}

@getter
fn dtype_str(&self) -> &'static str {
match self.inner.dtype() {
DType::F32 => "float32",
DType::F64 => "float64",
DType::I32 => "int32",
_ => "unknown",
}
}

@getter
fn shape(&self) -> Vec<usize> {
self.inner.shape().to_vec()
}
Comment thread
Aryan0819 marked this conversation as resolved.

// ── Python Buffer Protocol (PEP 3118) Implementation ──
unsafe fn __getbuffer__(
&self,
view: *mut Py_buffer,
_flags: c_int,
) -> PyResult<()> {
if view.is_null() {
return Err(PyValueError::new_err("Buffer view structure pointer is null"));
}

let item_size = self.inner.dtype().size_in_bytes() as isize;
let total_bytes = (self.inner.len() * self.inner.dtype().size_in_bytes()) as isize;

(*view).buf = self.inner.as_ptr() as *mut c_void;
(*view).obj = std::ptr::null_mut();
(*view).len = total_bytes;
(*view).itemsize = item_size;
(*view).readonly = 0;

(*view).format = match self.inner.dtype() {
DType::F32 => b"f\0".as_ptr() as *mut i8,
DType::F64 => b"d\0".as_ptr() as *mut i8,
DType::I32 => b"i\0".as_ptr() as *mut i8,
_ => b"B\0".as_ptr() as *mut i8,
};

(*view).ndim = self.inner.ndim() as i32;
(*view).shape = self.inner.shape().as_ptr() as *mut isize;
(*view).strides = self.inner.strides().as_ptr() as *mut isize;
(*view).suboffsets = std::ptr::null_mut();
(*view).internal = std::ptr::null_mut();

Ok(())
}
Comment thread
Aryan0819 marked this conversation as resolved.

unsafe fn __releasebuffer__(&self, _view: *mut Py_buffer) {
// Shared reference cleanup is handled implicitly through Rust dropped object cycles
}
}
38 changes: 38 additions & 0 deletions docs/src/dlpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use pyo3::prelude::*;
use pyo3::exceptions::PyRuntimeError;
use pyo3::ffi::{PyCapsule_New, PyCapsule_GetPointer};
use crate::tensor::PyTensor;
use std::os::raw::c_void;

const DL_CPU_DEVICE_TYPE: i32 = 1;

#[pymethods]
impl PyTensor {
pub fn __dlpack__<'py>(&self, py: Python<'py>, _stream: Option<&Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> {
// Request the raw DLManagedTensor representation from mohu-buffer / mohu-core primitives
let raw_dl_struct = Box::into_raw(Box::new(self.inner.to_dlpack_struct()));

unsafe {
let name = b"dltensor\0".as_ptr() as *const i8;
let capsule = PyCapsule_New(raw_dl_struct as *mut c_void, name, Some(dlpack_capsule_deleter));
if capsule.is_null() {
return Err(PyRuntimeError::new_err("Failed to construct DLPack capsule context"));
}
Ok(Bound::from_owned_ptr(py, capsule))
}
}

pub fn __dlpack_device__(&self) -> (i32, i32) {
// Return protocol device mapping tuple: (device_type, device_id)
(DL_CPU_DEVICE_TYPE, 0)
}
}

unsafe extern "C" fn dlpack_capsule_deleter(capsule: *mut pyo3::ffi::PyObject) {
let name = b"dltensor\0".as_ptr() as *const i8;
let raw_ptr = PyCapsule_GetPointer(capsule, name);
if !raw_ptr.is_null() {
// Safe context recovery of structural resource allocation memory context blocks
let _ = Box::from_raw(raw_ptr as *mut mohu_core::DLManagedTensor);
}
}
80 changes: 80 additions & 0 deletions docs/src/tensor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use pyo3::prelude::*;
use pyo3::ffi::{Py_buffer, PyBUF_WRITABLE};
use pyo3::exceptions::{PyValueError, PyTypeError};
use mohu_core::{Tensor, DType};
use std::os::raw::{c_void, c_int};

#[pyclass(name = "Tensor", subclass)]
pub struct PyTensor {
pub inner: Tensor,
}

#[pymethods]
impl PyTensor {
#[new]
fn new(shape: Vec<usize>, dtype_str: &str) -> PyResult<Self> {
let dtype = match dtype_str {
"float32" => DType::F32,
"float64" => DType::F64,
"int32" => DType::I32,
_ => return Err(PyValueError::new_err(format!("Unsupported dtype: {}", dtype_str))),
};

let inner = Tensor::empty(shape, dtype);
Ok(PyTensor { inner })
}

@getter
fn dtype_str(&self) -> &'static str {
Comment thread
Aryan0819 marked this conversation as resolved.
match self.inner.dtype() {
DType::F32 => "float32",
DType::F64 => "float64",
DType::I32 => "int32",
_ => "unknown",
}
}

@getter
fn shape(&self) -> Vec<usize> {
self.inner.shape().to_vec()
}

// ── Python Buffer Protocol (PEP 3118) Implementation ──
unsafe fn __getbuffer__(
&self,
view: *mut Py_buffer,
_flags: c_int,
) -> PyResult<()> {
Comment thread
Aryan0819 marked this conversation as resolved.
if view.is_null() {
return Err(PyValueError::new_err("Buffer view structure pointer is null"));
}

let item_size = self.inner.dtype().size_in_bytes() as isize;
let total_bytes = (self.inner.len() * self.inner.dtype().size_in_bytes()) as isize;

(*view).buf = self.inner.as_ptr() as *mut c_void;
(*view).obj = std::ptr::null_mut();
Comment thread
Aryan0819 marked this conversation as resolved.
(*view).len = total_bytes;
(*view).itemsize = item_size;
(*view).readonly = 0;

(*view).format = match self.inner.dtype() {
DType::F32 => b"f\0".as_ptr() as *mut i8,
DType::F64 => b"d\0".as_ptr() as *mut i8,
DType::I32 => b"i\0".as_ptr() as *mut i8,
_ => b"B\0".as_ptr() as *mut i8,
};

(*view).ndim = self.inner.ndim() as i32;
(*view).shape = self.inner.shape().as_ptr() as *mut isize;
(*view).strides = self.inner.strides().as_ptr() as *mut isize;
(*view).suboffsets = std::ptr::null_mut();
(*view).internal = std::ptr::null_mut();

Ok(())
}

unsafe fn __releasebuffer__(&self, _view: *mut Py_buffer) {
// Shared reference cleanup is handled implicitly through Rust dropped object cycles
}
}
Loading