| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #![cfg(feature = "gpu")] |
|
|
| pub mod sp_gpu; |
| pub mod tm_gpu; |
| pub mod fused; |
|
|
| #[cfg(test)] |
| mod tests; |
|
|
| use std::mem::ManuallyDrop; |
|
|
| use pyo3::prelude::*; |
| use pyo3::types::{PyDict, PyTuple}; |
| use numpy::{PyArray1, PyArray2, PyArrayMethods, PyReadonlyArray2, PyUntypedArrayMethods}; |
|
|
| use crate::region::HTMRegionCore; |
| use crate::sp::SpatialPoolerConfig; |
| use sp_gpu::SpatialPoolerGpu; |
| use tm_gpu::TemporalMemoryGpu; |
| use fused::FusedState; |
|
|
| |
| |
| |
| fn cai_parse(cai: &Bound<'_, PyDict>) -> PyResult<(u64, Vec<usize>, String)> { |
| |
| let data_obj = cai.get_item("data")? |
| .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("CAI missing 'data'"))?; |
| let data_tup: Bound<'_, PyTuple> = data_obj.downcast_into() |
| .map_err(|_| pyo3::exceptions::PyValueError::new_err("CAI 'data' must be a tuple"))?; |
| let ptr: u64 = data_tup.get_item(0)?.extract()?; |
|
|
| |
| let shape_obj = cai.get_item("shape")? |
| .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("CAI missing 'shape'"))?; |
| let shape_tup: Bound<'_, PyTuple> = shape_obj.downcast_into() |
| .map_err(|_| pyo3::exceptions::PyValueError::new_err("CAI 'shape' must be a tuple"))?; |
| let shape: Vec<usize> = (0..shape_tup.len()) |
| .map(|i| shape_tup.get_item(i).and_then(|v| v.extract::<usize>())) |
| .collect::<PyResult<Vec<_>>>()?; |
|
|
| |
| let typestr_obj = cai.get_item("typestr")? |
| .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("CAI missing 'typestr'"))?; |
| let typestr: String = typestr_obj.extract()?; |
|
|
| |
| if let Some(strides) = cai.get_item("strides")? { |
| if !strides.is_none() { |
| return Err(pyo3::exceptions::PyValueError::new_err( |
| "CAI 'strides' must be None (tensor must be contiguous)", |
| )); |
| } |
| } |
|
|
| Ok((ptr, shape, typestr)) |
| } |
|
|
| |
| #[pyclass(module = "htm_rust")] |
| pub struct HTMRegionGpu { |
| sp_gpu: SpatialPoolerGpu, |
| tm_gpu: TemporalMemoryGpu, |
| fused_state: FusedState, |
| n_columns: usize, |
| input_bits: usize, |
| cells_per_column: usize, |
| } |
|
|
| #[pymethods] |
| impl HTMRegionGpu { |
| #[new] |
| #[pyo3(signature = (input_bits, n_columns, cells_per_column, seed=42))] |
| fn new( |
| input_bits: usize, |
| n_columns: usize, |
| cells_per_column: usize, |
| seed: u64, |
| ) -> PyResult<Self> { |
| if input_bits == 0 || n_columns == 0 || cells_per_column == 0 { |
| return Err(pyo3::exceptions::PyValueError::new_err( |
| "input_bits, n_columns, cells_per_column must all be > 0", |
| )); |
| } |
| |
| let cpu_ref = HTMRegionCore::new(input_bits, n_columns, cells_per_column, seed); |
| let sp_cfg: &SpatialPoolerConfig = &cpu_ref.sp.cfg; |
| let sp_gpu = SpatialPoolerGpu::from_cpu(&cpu_ref.sp).map_err(|e| { |
| pyo3::exceptions::PyRuntimeError::new_err(format!( |
| "GPU SP init failed: {e:?}. Config: input_bits={}, n_columns={}", |
| sp_cfg.input_bits, sp_cfg.n_columns, |
| )) |
| })?; |
| let dev = sp_gpu.dev_ref().clone(); |
| let tm_gpu = TemporalMemoryGpu::new(dev.clone(), n_columns, cells_per_column).map_err(|e| { |
| pyo3::exceptions::PyRuntimeError::new_err(format!( |
| "GPU TM init failed: {e:?}", |
| )) |
| })?; |
| let initial_threshold = sp_gpu.initial_threshold_estimate(); |
| let fused_state = FusedState::new(dev, n_columns, cells_per_column, initial_threshold) |
| .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!( |
| "GPU fused state init failed: {e:?}", |
| )))?; |
| Ok(Self { |
| sp_gpu, |
| tm_gpu, |
| fused_state, |
| n_columns, |
| input_bits, |
| cells_per_column, |
| }) |
| } |
|
|
| #[getter] fn input_bits(&self) -> usize { self.input_bits } |
| #[getter] fn n_columns(&self) -> usize { self.n_columns } |
| #[getter] fn cells_per_column(&self) -> usize { self.cells_per_column } |
|
|
| |
| |
| |
| #[pyo3(signature = (inputs, learn=true))] |
| fn step_many_gpu<'py>( |
| &mut self, |
| py: Python<'py>, |
| inputs: PyReadonlyArray2<'py, bool>, |
| learn: bool, |
| ) -> PyResult<(Bound<'py, PyArray2<f32>>, Bound<'py, PyArray1<f32>>)> { |
| let shape = inputs.shape(); |
| if shape.len() != 2 { |
| return Err(pyo3::exceptions::PyValueError::new_err( |
| "inputs must be 2-D (T, input_bits)", |
| )); |
| } |
| let t = shape[0]; |
| let bits = shape[1]; |
| if bits != self.input_bits { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "inputs last dim {bits} != expected input_bits {}", |
| self.input_bits, |
| ))); |
| } |
| let slice = inputs.as_slice()?; |
| let n_cols = self.n_columns; |
| let input_vec: Vec<bool> = slice.to_vec(); |
|
|
| let result = py.allow_threads(|| -> Result<(Vec<u8>, Vec<f32>), String> { |
| |
| let sdr_u8_all: Vec<u8> = input_vec.iter().map(|&b| b as u8).collect(); |
| let inputs_dev = self |
| .sp_gpu |
| .dev_ref() |
| .htod_sync_copy(&sdr_u8_all) |
| .map_err(|e| format!("H2D inputs: {e:?}"))?; |
|
|
| |
| let mut cols_dev = self.sp_gpu.dev_ref() |
| .alloc_zeros::<u8>(t * n_cols) |
| .map_err(|e| format!("alloc cols: {e:?}"))?; |
| let mut anom_dev = self.sp_gpu.dev_ref() |
| .alloc_zeros::<f32>(t) |
| .map_err(|e| format!("alloc anom: {e:?}"))?; |
|
|
| |
| self.sp_gpu.step_batch_with_tm( |
| &inputs_dev, |
| t, |
| self.input_bits, |
| learn, |
| &mut cols_dev, |
| &mut anom_dev, |
| &mut self.tm_gpu, |
| ).map_err(|e| format!("step_batch_with_tm: {e:?}"))?; |
|
|
| |
| let cols_host: Vec<u8> = self.sp_gpu.dev_ref() |
| .dtoh_sync_copy(&cols_dev) |
| .map_err(|e| format!("D2H cols: {e:?}"))?; |
| let anom_host: Vec<f32> = self.sp_gpu.dev_ref() |
| .dtoh_sync_copy(&anom_dev) |
| .map_err(|e| format!("D2H anom: {e:?}"))?; |
|
|
| Ok((cols_host, anom_host)) |
| }); |
|
|
| let (cols_u8, anom) = result.map_err(pyo3::exceptions::PyRuntimeError::new_err)?; |
|
|
| let cols_f32: Vec<f32> = cols_u8.iter().map(|&b| b as f32).collect(); |
| let cols_arr = numpy::PyArray1::from_vec_bound(py, cols_f32) |
| .reshape([t, n_cols]) |
| .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; |
| let anom_arr = numpy::PyArray1::from_vec_bound(py, anom); |
| Ok((cols_arr, anom_arr)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyo3(signature = (sdr_cai, cols_cai, anom_cai, learn=true))] |
| fn step_many_cuda( |
| &mut self, |
| py: Python<'_>, |
| sdr_cai: &Bound<'_, PyDict>, |
| cols_cai: &Bound<'_, PyDict>, |
| anom_cai: &Bound<'_, PyDict>, |
| learn: bool, |
| ) -> PyResult<()> { |
| let (sdr_ptr, sdr_shape, sdr_type) = cai_parse(sdr_cai)?; |
| let (cols_ptr, cols_shape, cols_type) = cai_parse(cols_cai)?; |
| let (anom_ptr, anom_shape, anom_type) = cai_parse(anom_cai)?; |
|
|
| |
| if sdr_type != "|u1" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "sdr_cai typestr must be '|u1' (uint8), got {sdr_type}", |
| ))); |
| } |
| if cols_type != "|u1" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "cols_cai typestr must be '|u1' (uint8), got {cols_type}", |
| ))); |
| } |
| if anom_type != "<f4" && anom_type != "=f4" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "anom_cai typestr must be '<f4' (float32), got {anom_type}", |
| ))); |
| } |
|
|
| |
| if sdr_shape.len() != 2 || sdr_shape[1] != self.input_bits { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "sdr_cai shape {sdr_shape:?} != (T, {})", |
| self.input_bits, |
| ))); |
| } |
| let t = sdr_shape[0]; |
| if cols_shape != [t, self.n_columns] { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "cols_cai shape {cols_shape:?} != ({t}, {})", |
| self.n_columns, |
| ))); |
| } |
| if anom_shape != [t] { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "anom_cai shape {anom_shape:?} != ({t},)", |
| ))); |
| } |
|
|
| let dev = self.sp_gpu.dev_ref().clone(); |
| let n_cols = self.n_columns; |
| let input_bits = self.input_bits; |
|
|
| let result = py.allow_threads(|| -> Result<(), String> { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let inputs_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<u8>(sdr_ptr, t * input_bits) |
| }); |
| let mut cols_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<u8>(cols_ptr, t * n_cols) |
| }); |
| let mut anom_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<f32>(anom_ptr, t) |
| }); |
|
|
| self.sp_gpu.step_batch_with_tm( |
| &inputs_dev, |
| t, |
| input_bits, |
| learn, |
| &mut cols_dev, |
| &mut anom_dev, |
| &mut self.tm_gpu, |
| ).map_err(|e| format!("step_batch_with_tm: {e:?}"))?; |
|
|
| |
| |
| |
| |
| |
| dev.synchronize().map_err(|e| format!("sync: {e:?}"))?; |
| Ok(()) |
| }); |
|
|
| result.map_err(pyo3::exceptions::PyRuntimeError::new_err)?; |
| Ok(()) |
| } |
|
|
| |
| fn reset(&mut self) -> PyResult<()> { |
| self.tm_gpu.reset().map_err(|e| { |
| pyo3::exceptions::PyRuntimeError::new_err(format!("GPU TM reset: {e:?}")) |
| })?; |
| self.fused_state.reset().map_err(|e| { |
| pyo3::exceptions::PyRuntimeError::new_err(format!("GPU fused reset: {e:?}")) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyo3(signature = (sdr_cai, cols_cai, anom_cai, learn=true))] |
| fn step_many_fused_cuda( |
| &mut self, |
| py: Python<'_>, |
| sdr_cai: &Bound<'_, PyDict>, |
| cols_cai: &Bound<'_, PyDict>, |
| anom_cai: &Bound<'_, PyDict>, |
| learn: bool, |
| ) -> PyResult<()> { |
| let (sdr_ptr, sdr_shape, sdr_type) = cai_parse(sdr_cai)?; |
| let (cols_ptr, cols_shape, cols_type) = cai_parse(cols_cai)?; |
| let (anom_ptr, anom_shape, anom_type) = cai_parse(anom_cai)?; |
|
|
| if sdr_type != "|u1" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "sdr_cai typestr must be '|u1' (uint8), got {sdr_type}", |
| ))); |
| } |
| if cols_type != "|u1" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "cols_cai typestr must be '|u1' (uint8), got {cols_type}", |
| ))); |
| } |
| if anom_type != "<f4" && anom_type != "=f4" { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "anom_cai typestr must be '<f4' (float32), got {anom_type}", |
| ))); |
| } |
|
|
| if sdr_shape.len() != 2 || sdr_shape[1] != self.input_bits { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "sdr_cai shape {sdr_shape:?} != (T, {})", |
| self.input_bits, |
| ))); |
| } |
| let t = sdr_shape[0]; |
| if cols_shape != [t, self.n_columns] { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "cols_cai shape {cols_shape:?} != ({t}, {})", |
| self.n_columns, |
| ))); |
| } |
| if anom_shape != [t] { |
| return Err(pyo3::exceptions::PyValueError::new_err(format!( |
| "anom_cai shape {anom_shape:?} != ({t},)", |
| ))); |
| } |
|
|
| let dev = self.sp_gpu.dev_ref().clone(); |
| let n_cols = self.n_columns; |
| let input_bits = self.input_bits; |
|
|
| let result = py.allow_threads(|| -> Result<(), String> { |
| let inputs_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<u8>(sdr_ptr, t * input_bits) |
| }); |
| let mut cols_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<u8>(cols_ptr, t * n_cols) |
| }); |
| let mut anom_dev = ManuallyDrop::new(unsafe { |
| dev.upgrade_device_ptr::<f32>(anom_ptr, t) |
| }); |
|
|
| fused::launch_fused( |
| &mut self.sp_gpu, |
| &mut self.tm_gpu, |
| &mut self.fused_state, |
| &inputs_dev, |
| &mut cols_dev, |
| &mut anom_dev, |
| t, |
| input_bits, |
| learn, |
| ).map_err(|e| format!("launch_fused: {e:?}"))?; |
|
|
| dev.synchronize().map_err(|e| format!("sync: {e:?}"))?; |
| Ok(()) |
| }); |
|
|
| result.map_err(pyo3::exceptions::PyRuntimeError::new_err)?; |
| Ok(()) |
| } |
| } |
|
|
| pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { |
| m.add_class::<HTMRegionGpu>()?; |
| Ok(()) |
| } |
|
|