#[cfg(feature = "parallel-rows")]
use std::sync::Arc;
use crate::{Result, invalid};
#[path = "parallel_tile.rs"]
mod tile;
pub const MAX_ROW_WORKERS: usize = 16;
#[derive(Clone)]
pub struct RowParallelism {
workers: usize,
#[cfg(feature = "parallel-rows")]
pool: Option<Arc<rayon::ThreadPool>>,
}
#[cfg(all(test, feature = "parallel-rows"))]
mod tests {
use std::sync::Arc;
use bonsai_ternary::pack_ptq;
use super::super::{MatvecBackend, MatvecScratch, matvec_into};
use super::RowParallelism;
use crate::{Result, invalid, model::Tensor};
#[test]
fn pinned_parallel_rows_preserve_both_activation_contracts() -> Result<()> {
for workers in [2, 4] {
let policy = RowParallelism::new(workers)?;
for (kind, columns) in [(142, 5120), (142, 384), (143, 6144)] {
let mut block = [0x66_u8; 34];
block[..2].copy_from_slice(&0x3555_u16.to_le_bytes());
let encoded = if kind == 143 {
pack_ptq(&block)?.to_vec()
} else {
block.to_vec()
};
let tensor = Tensor {
name: "pinned-row-control".into(),
dimensions: vec![columns, 7],
kind,
data: Arc::from(encoded.repeat(columns / 128 * 7)),
};
let input: Vec<_> = (0..columns)
.map(|index| [1.5, -127.0, 0.0, -0.0, 63.5][index % 5])
.collect();
for backend in [MatvecBackend::Scalar, MatvecBackend::Auto] {
let mut serial = MatvecScratch::with_pinned_backend(backend);
let mut parallel = MatvecScratch::with_pinned_backend(backend);
parallel.set_row_parallelism(policy.clone());
let (mut expected, mut actual) = (Vec::new(), Vec::new());
matvec_into(&tensor, &input, &mut expected, &mut serial)?;
matvec_into(&tensor, &input, &mut actual, &mut parallel)?;
assert_eq!(
actual
.iter()
.map(|value| value.to_bits())
.collect::<Vec<_>>(),
expected
.iter()
.map(|value| value.to_bits())
.collect::<Vec<_>>()
);
assert_eq!(parallel.statistics().parallel_row_operations, 1);
assert_eq!(parallel.row_workers(), workers);
}
}
}
Ok(())
}
#[test]
fn row_tasks_borrow_original_storage_and_return_first_row_failure() -> Result<()> {
let policy = RowParallelism::new(2)?;
let data = [0_u8, 1, 2, 3, 4, 5, 6, 7];
let mut output = Vec::with_capacity(data.len());
let mut errors = Vec::new();
let error = policy.fill(&data, 1, &mut output, &mut errors, |row| {
assert_eq!(
row.as_ptr(),
data.as_ptr().wrapping_add(usize::from(row[0]))
);
assert!(
std::thread::current()
.name()
.is_some_and(|name| name.starts_with("prism-row-"))
);
if row[0] == 3 || row[0] == 7 {
Err(invalid(format!("rejected row {}", row[0])))
} else {
Ok(f32::from(row[0]))
}
});
assert_eq!(
error.map_err(|error| error.to_string()),
Err("rejected row 3".to_owned())
);
assert!(errors.is_empty());
assert!(errors.capacity() <= 2);
Ok(())
}
#[test]
fn update_rows_partitions_in_place_and_reports_earliest_chunk_failure() -> Result<()> {
let policy = RowParallelism::new(4)?;
let mut values = vec![1.0_f32; 10];
policy.update_rows(&mut values, |base, chunk| {
for (offset, target) in chunk.iter_mut().enumerate() {
*target += f32::from(u8::try_from(base + offset)?);
}
Ok(())
})?;
let expected: Vec<f32> = (0_u8..10).map(|idx| 1.0 + f32::from(idx)).collect();
assert_eq!(values, expected);
let err = policy.update_rows(&mut values, |base, _chunk| {
if base >= 3 {
Err(invalid(format!("chunk-at-{base}")))
} else {
Ok(())
}
});
assert_eq!(error_text(err), Err("chunk-at-3".to_owned()));
let mut bytes = vec![0_u8; 24];
policy.update_chunks(&mut bytes, 4, |base_unit, chunk| {
for (unit_offset, unit) in chunk.as_chunks_mut::<4>().0.iter_mut().enumerate() {
let value = u8::try_from(base_unit + unit_offset)?;
unit.fill(value);
}
Ok(())
})?;
for (unit, chunk) in bytes.as_chunks::<4>().0.iter().enumerate() {
assert_eq!(chunk, &[u8::try_from(unit)?; 4]);
}
assert!(policy.update_chunks(&mut bytes, 0, |_, _| Ok(())).is_err());
assert!(policy.update_chunks(&mut bytes, 5, |_, _| Ok(())).is_err());
Ok(())
}
fn error_text(result: Result<()>) -> std::result::Result<(), String> {
result.map_err(|error| error.to_string())
}
}
impl Default for RowParallelism {
fn default() -> Self {
Self {
workers: 1,
#[cfg(feature = "parallel-rows")]
pool: None,
}
}
}
impl RowParallelism {
pub fn new(workers: usize) -> Result<Self> {
if !(1..=MAX_ROW_WORKERS).contains(&workers) {
return Err(invalid("row worker count must be from 1 through 16"));
}
if workers == 1 {
return Ok(Self::default());
}
#[cfg(feature = "parallel-rows")]
{
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(workers)
.stack_size(512 * 1024)
.thread_name(|index| format!("prism-row-{index}"))
.build()?;
Ok(Self {
workers,
pool: Some(Arc::new(pool)),
})
}
#[cfg(not(feature = "parallel-rows"))]
Err(invalid("multiple row workers require parallel-rows"))
}
#[must_use]
pub const fn workers(&self) -> usize {
self.workers
}
pub fn update_rows(
&self,
target: &mut [f32],
update: impl Fn(usize, &mut [f32]) -> Result<()> + Sync,
) -> Result<()> {
self.update_chunks(target, 1, update)
}
pub fn update_chunks<T: Send>(
&self,
target: &mut [T],
unit_size: usize,
update: impl Fn(usize, &mut [T]) -> Result<()> + Sync,
) -> Result<()> {
if unit_size == 0 || !target.len().is_multiple_of(unit_size) {
return Err(invalid(
"target length must be a multiple of a positive unit size",
));
}
let units = target.len() / unit_size;
#[cfg(not(feature = "parallel-rows"))]
if self.uses_pool(units) {
return Err(invalid("multiple row workers require parallel-rows"));
}
#[cfg(feature = "parallel-rows")]
if let Some(pool) = &self.pool
&& units > 1
{
use rayon::prelude::*;
let task_units = units.div_ceil(self.workers);
let task_elems = task_units * unit_size;
let tasks = units.div_ceil(task_units);
let mut errors = Vec::new();
errors.try_reserve_exact(tasks)?;
errors.resize_with(tasks, || None);
pool.install(|| {
target
.par_chunks_mut(task_elems)
.zip(errors.par_iter_mut())
.enumerate()
.for_each(|(task, (chunk, error))| {
if let Err(failure) = update(task * task_units, chunk) {
*error = Some(failure);
}
});
});
if let Some(failure) = errors.into_iter().flatten().next() {
return Err(failure);
}
return Ok(());
}
update(0, target)
}
pub(super) const fn uses_pool(&self, rows: usize) -> bool {
self.workers > 1 && rows > 1
}
pub(super) fn fill(
&self,
data: &[u8],
row_bytes: usize,
output: &mut Vec<f32>,
errors: &mut Vec<Option<crate::Error>>,
dot: impl Fn(&[u8]) -> Result<f32> + Sync,
) -> Result<()> {
errors.clear();
#[cfg(not(feature = "parallel-rows"))]
if self.uses_pool(data.len() / row_bytes) {
return Err(invalid("multiple row workers require parallel-rows"));
}
#[cfg(feature = "parallel-rows")]
if let Some(pool) = &self.pool
&& data.len() / row_bytes > 1
{
use rayon::prelude::*;
let rows = data.len() / row_bytes;
let task_rows = rows.div_ceil(self.workers);
let tasks = rows.div_ceil(task_rows);
errors.try_reserve_exact(tasks)?;
errors.resize_with(tasks, || None);
output.resize(rows, 0.0);
pool.install(|| {
output
.par_chunks_mut(task_rows)
.zip(errors.par_iter_mut())
.enumerate()
.for_each(|(task, (chunk, error))| {
let start = task * task_rows * row_bytes;
let bytes = &data[start..start + chunk.len() * row_bytes];
for (value, row) in chunk.iter_mut().zip(bytes.chunks_exact(row_bytes)) {
match dot(row) {
Ok(result) => *value = result,
Err(failure) => {
*error = Some(failure);
break;
}
}
}
});
});
let failure = errors.iter_mut().find_map(Option::take);
errors.clear();
return failure.map_or(Ok(()), Err);
}
for bytes in data.chunks_exact(row_bytes) {
output.push(dot(bytes)?);
}
Ok(())
}
}