use super::*;
pub(super) fn view_bounds(
shape: &[usize],
strides: &[i64],
byte_offset: usize,
dtype: DataType,
buffer_len: usize,
) -> Result<()> {
let esize = dtype.byte_size();
if esize == 0 {
let storage_bytes =
onnx_runtime_ir::checked_expected_bytes(dtype, shape).ok_or_else(|| {
SessionError::ShapeOverflow {
value: "sub-byte tensor view".to_string(),
dims: shape.to_vec(),
}
})?;
let need =
byte_offset
.checked_add(storage_bytes)
.ok_or_else(|| SessionError::ShapeOverflow {
value: "sub-byte tensor view byte offset".to_string(),
dims: shape.to_vec(),
})?;
if need > buffer_len {
return Err(SessionError::from(
onnx_runtime_ep_api::EpError::InvalidTensorView {
reason: format!(
"sub-byte view needs {need} bytes but backing allocation is {buffer_len}"
),
},
));
}
return Ok(());
}
view_in_bounds(shape, strides, byte_offset, esize, buffer_len)?;
Ok(())
}
pub(super) fn gather_view(
src: &[u8],
shape: &[usize],
strides: &[i64],
byte_offset: usize,
esize: usize,
) -> Vec<u8> {
let n: usize = shape.iter().product();
let mut out = vec![0u8; n * esize];
gather_view_into(&mut out, src, shape, strides, byte_offset, esize);
out
}
pub(super) fn gather_view_into(
dst: &mut [u8],
src: &[u8],
shape: &[usize],
strides: &[i64],
byte_offset: usize,
esize: usize,
) {
let n: usize = shape.iter().product();
debug_assert_eq!(dst.len(), n * esize);
if n == 0 {
return;
}
let (shape, strides) = collapse_view(shape, strides);
gather_collapsed(src, dst, &shape, &strides, byte_offset, esize);
}
fn collapse_view(shape: &[usize], strides: &[i64]) -> (Vec<usize>, Vec<i64>) {
let mut dims: Vec<(usize, i64)> = shape
.iter()
.zip(strides)
.filter(|&(&dim, _)| dim != 1)
.map(|(&dim, &stride)| (dim, stride))
.collect();
if dims.is_empty() {
return (vec![1], vec![0]);
}
let mut fused: Vec<(usize, i64)> = Vec::with_capacity(dims.len());
for (dim, stride) in dims.drain(..) {
match fused.last_mut() {
Some((prev_dim, prev_stride)) if *prev_stride == stride * dim as i64 => {
*prev_dim *= dim;
*prev_stride = stride;
}
_ => fused.push((dim, stride)),
}
}
fused.into_iter().unzip()
}
const MIN_PARALLEL_GATHER_BYTES: usize = 256 * 1024;
fn gather_collapsed(
src: &[u8],
out: &mut [u8],
shape: &[usize],
strides: &[i64],
byte_offset: usize,
esize: usize,
) {
let rank = shape.len();
let (block_elems, outer_rank) = if strides[rank - 1] == 1 {
(shape[rank - 1], rank - 1)
} else {
(1, rank)
};
let block = block_elems * esize;
if outer_rank == 0 {
out[..block].copy_from_slice(&src[byte_offset..byte_offset + block]);
return;
}
let outer_shape = &shape[..outer_rank];
let outer_strides = &strides[..outer_rank];
let blocks: usize = outer_shape.iter().product();
let src_offset_of = |mut k: usize| -> usize {
let mut off = byte_offset as i64;
for axis in (0..outer_rank).rev() {
let digit = k % outer_shape[axis];
k /= outer_shape[axis];
off += digit as i64 * outer_strides[axis] * esize as i64;
}
off as usize
};
let body = |dst: &mut [u8], first_block: usize| {
let mut idx = vec![0usize; outer_rank];
let mut off = src_offset_of(first_block);
{
let mut k = first_block;
for axis in (0..outer_rank).rev() {
idx[axis] = k % outer_shape[axis];
k /= outer_shape[axis];
}
}
let mut written = 0usize;
loop {
dst[written..written + block].copy_from_slice(&src[off..off + block]);
written += block;
if written == dst.len() {
break;
}
let mut axis = outer_rank;
loop {
axis -= 1;
idx[axis] += 1;
off = (off as i64 + outer_strides[axis] * esize as i64) as usize;
if idx[axis] < outer_shape[axis] {
break;
}
off = (off as i64 - outer_shape[axis] as i64 * outer_strides[axis] * esize as i64)
as usize;
idx[axis] = 0;
if axis == 0 {
break;
}
}
}
};
let workers = rayon::current_num_threads().max(1);
if workers > 1 && blocks > 1 && blocks * block >= MIN_PARALLEL_GATHER_BYTES {
use rayon::prelude::*;
let per_task = blocks.div_ceil(workers).max(1);
out.par_chunks_mut(per_task * block)
.enumerate()
.for_each(|(task, chunk)| body(chunk, task * per_task));
} else {
body(out, 0);
}
}
pub(super) fn checked_numel(dims: &[usize], value: impl FnOnce() -> String) -> Result<usize> {
let mut acc = 1usize;
for &d in dims {
acc = match acc.checked_mul(d) {
Some(n) => n,
None => {
return Err(SessionError::ShapeOverflow {
value: value(),
dims: dims.to_vec(),
});
}
};
}
Ok(acc)
}
pub(super) fn checked_storage_bytes(
dtype: DataType,
numel: usize,
value: impl FnOnce() -> String,
dims: &[usize],
) -> Result<usize> {
dtype
.checked_storage_bytes(numel)
.ok_or_else(|| SessionError::ShapeOverflow {
value: value(),
dims: dims.to_vec(),
})
}
pub(super) fn effective_opset(graph: &Graph, node: &Node) -> u64 {
graph.effective_opset(node).unwrap_or_else(|| {
unreachable!(
"internal invariant violated: node #{} ({}::{}) has no opset import",
node.id.0,
if node.domain.is_empty() {
"ai.onnx"
} else {
&node.domain
},
node.op_type
)
})
}
pub(super) fn substitute(shape: &Shape, bindings: &HashMap<SymbolId, usize>) -> Option<Vec<usize>> {
shape
.iter()
.map(|d| match d {
Dim::Static(n) => Some(*n),
Dim::Symbolic(s) => bindings.get(s).copied(),
})
.collect()
}
pub(super) fn substitute_into(
shape: &Shape,
bindings: &HashMap<SymbolId, usize>,
out: &mut Vec<usize>,
) -> bool {
out.clear();
for d in shape {
match d {
Dim::Static(n) => out.push(*n),
Dim::Symbolic(s) => match bindings.get(s) {
Some(&v) => out.push(v),
None => {
out.clear();
return false;
}
},
}
}
true
}
pub(super) fn bytes_as_i64(bytes: &[u8], dtype: DataType) -> Option<Vec<i64>> {
match dtype {
DataType::Int64 => onnx_runtime_ir::read_vec_le(bytes).ok(),
DataType::Int32 => onnx_runtime_ir::read_vec_le::<i32>(bytes)
.ok()
.map(|values| values.into_iter().map(i64::from).collect()),
DataType::Bool => Some(bytes.iter().map(|&value| i64::from(value != 0)).collect()),
_ => None,
}
}
pub(super) fn bytes_as_f64(bytes: &[u8], dtype: DataType) -> Option<Vec<f64>> {
match dtype {
DataType::Float32 => onnx_runtime_ir::read_vec_le::<f32>(bytes)
.ok()
.map(|values| values.into_iter().map(f64::from).collect()),
DataType::Float64 => onnx_runtime_ir::read_vec_le(bytes).ok(),
_ => None,
}
}
pub(super) fn bounded_shape_input(dtype: DataType, shape: &[usize]) -> bool {
if !matches!(dtype, DataType::Bool | DataType::Int32 | DataType::Int64) {
return false;
}
if shape.len() > 1 {
return false;
}
shape
.iter()
.try_fold(1usize, |count, &dim| count.checked_mul(dim))
.is_some_and(|count| count <= MAX_SHAPE_DATA_ELEMS)
}
const MAX_COMPRESS_CONDITION_ELEMS: usize = 1 << 20;
pub(super) fn bounded_compress_condition(dtype: DataType, shape: &[usize]) -> bool {
dtype == DataType::Bool && shape.len() == 1 && shape[0] <= MAX_COMPRESS_CONDITION_ELEMS
}
pub(super) fn reads_float_shape_input(node: &Node, input_index: usize, opset: u64) -> bool {
node.is_default_domain()
&& ((node.op_type == "Resize" && input_index == if opset == 10 { 1 } else { 2 })
|| (node.op_type == "NonMaxSuppression" && matches!(input_index, 0 | 1 | 3 | 4)))
}
pub(super) fn kernel_input_uses_physical_capacity(node: &Node, input_index: usize) -> bool {
if node.domain == "com.microsoft"
&& node.op_type == "GroupQueryAttention"
&& matches!(input_index, 3 | 4)
{
return true;
}
node.is_default_domain()
&& node.op_type == "Attention"
&& matches!(input_index, 4 | 5)
&& node.inputs.get(3).is_some_and(Option::is_some)
|| (
node.domain == "pkg.nxrt"
&& node.op_type == "IndexShare"
&& matches!(input_index, 3 | 4)
&& node.outputs.len() == 3
&& node.inputs.get(6).is_some_and(Option::is_some)
)
|| (
node.domain == KV_CAPACITY_APPEND_DOMAIN
&& node.op_type == KV_CAPACITY_APPEND_OP
&& input_index == 0
&& node.inputs.get(2).is_some_and(Option::is_some)
)
}
pub(super) fn kernel_input_uses_padded_capacity(node: &Node, input_index: usize) -> bool {
node.is_default_domain()
&& input_index == 0
&& matches!(node.op_type.as_str(), "Shape" | "ReduceSum")
}
pub(super) fn binding_shape_result_is_observed(graph: &Graph, input: ValueId) -> bool {
let shape_outputs = graph
.nodes
.iter()
.filter(|(_, node)| {
node.is_default_domain()
&& node.op_type == "Shape"
&& node.inputs.first().copied().flatten() == Some(input)
})
.flat_map(|(_, node)| node.outputs.iter().copied())
.collect::<HashSet<_>>();
if shape_outputs.is_empty() {
return false;
}
graph
.outputs
.iter()
.any(|output| shape_outputs.contains(output))
|| graph.nodes.iter().any(|(_, node)| {
node.inputs
.iter()
.flatten()
.any(|input| shape_outputs.contains(input))
})
}
pub(super) fn describe_non_padded_consumer(node: &Node, input_index: usize) -> Option<String> {
if kernel_input_uses_padded_capacity(node, input_index) {
return None;
}
Some(format!("{}[input {input_index}]", describe_node(node)))
}
pub(super) fn is_additive_mask_builder_op(node: &Node) -> bool {
node.is_default_domain()
&& matches!(
node.op_type.as_str(),
"CumSum" | "Unsqueeze" | "Cast" | "GreaterOrEqual" | "And" | "Where" | "Slice" | "Sub"
)
}
pub(super) fn is_capacity_form_attention_mask_input(node: &Node, input_index: usize) -> bool {
input_index == 3
&& node.is_default_domain()
&& node.op_type == "Attention"
&& node.inputs.get(4).is_some_and(Option::is_some)
&& node.inputs.get(5).is_some_and(Option::is_some)
&& kernel_input_uses_physical_capacity(node, 4)
}
pub(super) const KV_CAPACITY_APPEND_DOMAIN: &str = "pkg.nxrt";
pub(super) const KV_CAPACITY_APPEND_OP: &str = "KvCacheCapacityAppend";
pub(super) fn is_kv_cache_growth_concat(graph: &Graph, node: &Node) -> bool {
((node.is_default_domain() && node.op_type == "Concat")
|| (node.domain == KV_CAPACITY_APPEND_DOMAIN && node.op_type == KV_CAPACITY_APPEND_OP))
&& node
.inputs
.first()
.copied()
.flatten()
.is_some_and(|input| graph.inputs.contains(&input))
&& node
.outputs
.first()
.is_some_and(|output| graph.outputs.contains(output))
}
fn is_shape_preserving_relabel_op(node: &Node) -> bool {
node.is_default_domain()
&& matches!(
node.op_type.as_str(),
"Transpose"
| "Reshape"
| "Unsqueeze"
| "Squeeze"
| "Cast"
| "Mul"
| "Div"
| "Slice"
| "Expand"
)
}
pub(super) fn kv_cache_growth_concat_source(graph: &Graph, value: ValueId) -> Option<NodeId> {
use std::collections::{HashMap, HashSet, VecDeque};
let mut producer: HashMap<ValueId, NodeId> = HashMap::new();
for (node_id, node) in graph.nodes.iter() {
for out in &node.outputs {
producer.insert(*out, node_id);
}
}
let mut seen: HashSet<ValueId> = HashSet::new();
let mut frontier: VecDeque<ValueId> = VecDeque::new();
frontier.push_back(value);
while let Some(current) = frontier.pop_front() {
if !seen.insert(current) {
continue;
}
let Some(&node_id) = producer.get(¤t) else {
continue;
};
let node = graph.node(node_id);
if is_kv_cache_growth_concat(graph, node) {
return Some(node_id);
}
if node.is_default_domain() && node.op_type == "MatMul" {
if let Some(Some(rhs)) = node.inputs.get(1) {
frontier.push_back(*rhs);
}
continue;
}
if is_shape_preserving_relabel_op(node) {
for input in node.inputs.iter().flatten() {
frontier.push_back(*input);
}
}
}
None
}
pub(super) fn derives_from_kv_cache_growth(graph: &Graph, value: ValueId) -> bool {
kv_cache_growth_concat_source(graph, value).is_some()
}
fn forward_shape_preserving_matmul_consumers(
graph: &Graph,
start: ValueId,
) -> Vec<(NodeId, usize)> {
use std::collections::{HashSet, VecDeque};
let mut consumers: std::collections::HashMap<ValueId, Vec<(NodeId, usize)>> =
std::collections::HashMap::new();
for (node_id, node) in graph.nodes.iter() {
for (slot, value) in node.inputs.iter().enumerate() {
if let Some(vid) = value {
consumers.entry(*vid).or_default().push((node_id, slot));
}
}
}
let mut seen: HashSet<ValueId> = HashSet::new();
let mut frontier: VecDeque<ValueId> = VecDeque::new();
frontier.push_back(start);
let mut hits = Vec::new();
while let Some(value) = frontier.pop_front() {
if !seen.insert(value) {
continue;
}
for &(node_id, slot) in consumers.get(&value).map_or(&[][..], Vec::as_slice) {
let node = graph.node(node_id);
if node.is_default_domain() && node.op_type == "MatMul" {
hits.push((node_id, slot));
continue;
}
if is_shape_preserving_relabel_op(node) {
frontier.extend(node.outputs.iter().copied());
}
}
}
hits
}
pub(super) fn mask_binding_feeds_capacity_form_attention(graph: &Graph, mask: ValueId) -> bool {
mask_cone_rejection(graph, mask, ShapeConsumptionPolicy::Disqualify).is_none()
}
pub(super) fn mask_cone_rejection(
graph: &Graph,
mask: ValueId,
shape_policy: ShapeConsumptionPolicy,
) -> Option<String> {
mask_binding_feeds_capacity_form_attention_impl(graph, mask, shape_policy).err()
}
pub(super) fn mask_binding_feeds_additive_causal_builder(graph: &Graph, mask: ValueId) -> bool {
mask_binding_feeds_capacity_form_attention_impl(graph, mask, ShapeConsumptionPolicy::Allow)
.is_ok()
}
pub(super) fn kv_capacity_write_eligible_concats(graph: &Graph) -> HashSet<NodeId> {
let mut eligible = HashSet::new();
for &input in &graph.inputs {
if let Ok(concats) = mask_binding_feeds_capacity_form_attention_impl(
graph,
input,
ShapeConsumptionPolicy::Allow,
) {
eligible.extend(concats);
}
}
eligible
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum ShapeConsumptionPolicy {
Disqualify,
Allow,
}
enum ConsumerRole {
Sink,
InvariantLeaf,
Propagate,
Mixes(String),
}
fn classify_mask_consumer(
graph: &Graph,
node: &Node,
slot: usize,
mask: ValueId,
consumers: &std::collections::HashMap<ValueId, Vec<(NodeId, usize)>>,
shape_policy: ShapeConsumptionPolicy,
) -> ConsumerRole {
if is_capacity_form_attention_mask_input(node, slot) {
return ConsumerRole::Sink;
}
if kernel_input_uses_padded_capacity(node, slot) {
if node.op_type == "Shape" && shape_policy == ShapeConsumptionPolicy::Disqualify {
let consumed = node.outputs.iter().any(|out| consumers.contains_key(out));
if consumed {
return ConsumerRole::Mixes(format!(
"{} output is consumed, so the padded max_len would leak into \
width-sensitive arithmetic (the prefill query-position window)",
describe_node(node)
));
}
}
return ConsumerRole::InvariantLeaf;
}
if node.is_default_domain() && node.op_type == "Expand" && slot == 0 {
return if expand_target_is_mask_derived(graph, node, mask) {
ConsumerRole::Propagate
} else {
ConsumerRole::Mixes(format!(
"{}[input {slot}] broadcasts the mask to a target shape not derived from \
Shape(mask), so the frozen width would not match the target",
describe_node(node)
))
};
}
if is_additive_mask_builder_op(node) {
return ConsumerRole::Propagate;
}
if node.is_default_domain() && node.op_type == "Add" && node.inputs.len() == 2 {
let other = node.inputs[1 - slot];
return if other.is_some_and(|value| derives_from_kv_cache_growth(graph, value)) {
ConsumerRole::Propagate
} else {
ConsumerRole::Mixes(format!(
"{}[input {slot}] adds the mask to an operand that does not itself derive \
from a KV-cache append, so freezing would compare max_len against a value \
still at its logical length",
describe_node(node)
))
};
}
if node.is_default_domain() && node.op_type == "Concat" {
let independent = node.inputs.iter().enumerate().all(|(i, input)| {
i == slot || input.is_none_or(|value| !derives_from_kv_cache_growth(graph, value))
});
return if independent {
ConsumerRole::Propagate
} else {
ConsumerRole::Mixes(format!(
"{}[input {slot}] concatenates the mask-derived value with an operand that \
also derives from a KV-cache append, so the two would not stay the same \
size once the mask is frozen",
describe_node(node)
))
};
}
if node.is_default_domain() && node.op_type == "Softmax" {
let axis = node.attr("axis").and_then(Attribute::as_int);
let normalizes_length_axis = match axis {
Some(-1) => true,
Some(_) => false,
None => effective_opset(graph, node) >= 13,
};
return if normalizes_length_axis {
ConsumerRole::Sink
} else {
ConsumerRole::Mixes(format!(
"{} normalizes over an axis other than the last (or defaults to opset <= 12's \
axis=1 coerce-to-2D semantics, which is not confirmed to be the last axis \
alone), so it would not neutralise the mask's padded lanes before a \
non-padding-aware consumer",
describe_node(node)
))
};
}
ConsumerRole::Mixes(format!(
"{}[input {slot}] is outside the additive causal-mask builder set, so it \
sources the mask length axis from another value",
describe_node(node)
))
}
fn expand_target_is_mask_derived(graph: &Graph, expand: &Node, mask: ValueId) -> bool {
use std::collections::{HashSet, VecDeque};
let mut producer: std::collections::HashMap<ValueId, NodeId> = std::collections::HashMap::new();
for (node_id, node) in graph.nodes.iter() {
for out in &node.outputs {
producer.insert(*out, node_id);
}
}
let Some(Some(target)) = expand.inputs.get(1).copied() else {
return false;
};
let mut seen: HashSet<ValueId> = HashSet::new();
let mut frontier: VecDeque<ValueId> = VecDeque::new();
frontier.push_back(target);
while let Some(value) = frontier.pop_front() {
if !seen.insert(value) {
continue;
}
let Some(&node_id) = producer.get(&value) else {
continue;
};
let node = graph.node(node_id);
if node.op_type == "Shape" && node.inputs.first().copied().flatten() == Some(mask) {
return true;
}
for input in node.inputs.iter().flatten() {
frontier.push_back(*input);
}
}
false
}
fn mask_binding_feeds_capacity_form_attention_impl(
graph: &Graph,
mask: ValueId,
shape_policy: ShapeConsumptionPolicy,
) -> std::result::Result<HashSet<NodeId>, String> {
use std::collections::{HashMap, HashSet, VecDeque};
let mut consumers: HashMap<ValueId, Vec<(NodeId, usize)>> = HashMap::new();
for (node_id, node) in graph.nodes.iter() {
for (slot, value) in node.inputs.iter().enumerate() {
if let Some(vid) = value {
consumers.entry(*vid).or_default().push((node_id, slot));
}
}
}
let graph_outputs: HashSet<ValueId> = graph.outputs.iter().copied().collect();
let mut visited: HashSet<ValueId> = HashSet::new();
let mut frontier: VecDeque<ValueId> = VecDeque::new();
frontier.push_back(mask);
let mut reached_attention = false;
let mut score_role_concats: HashSet<NodeId> = HashSet::new();
let mut sink_softmax_outputs: Vec<ValueId> = Vec::new();
while let Some(value) = frontier.pop_front() {
if !visited.insert(value) {
continue;
}
if graph_outputs.contains(&value) {
return Err(format!(
"mask-derived value {value:?} escapes as a graph output, so freezing the mask \
to physical width would leak the padded max_len to whatever consumes it"
));
}
for &(node_id, slot) in consumers.get(&value).map_or(&[][..], Vec::as_slice) {
let node = graph.node(node_id);
match classify_mask_consumer(graph, node, slot, mask, &consumers, shape_policy) {
ConsumerRole::Sink => {
reached_attention = true;
if node.is_default_domain() && node.op_type == "Softmax" {
sink_softmax_outputs.extend(node.outputs.iter().copied());
}
}
ConsumerRole::InvariantLeaf => {}
ConsumerRole::Propagate => {
if node.is_default_domain() && node.op_type == "Add" && node.inputs.len() == 2 {
let other = node.inputs[1 - slot];
if let Some(concat_id) =
other.and_then(|value| kv_cache_growth_concat_source(graph, value))
{
score_role_concats.insert(concat_id);
}
}
frontier.extend(node.outputs.iter().copied());
}
ConsumerRole::Mixes(reason) => return Err(reason),
}
}
}
if !reached_attention {
return Err(String::from(
"the mask cone never reached a capacity-form Attention mask input",
));
}
let mut eligible = score_role_concats;
for output in sink_softmax_outputs {
for (matmul_id, probs_slot) in forward_shape_preserving_matmul_consumers(graph, output) {
let matmul = graph.node(matmul_id);
if matmul.inputs.len() != 2 {
continue;
}
let Some(other) = matmul.inputs[1 - probs_slot] else {
continue;
};
if let Some(concat_id) = kv_cache_growth_concat_source(graph, other) {
eligible.insert(concat_id);
}
}
}
Ok(eligible)
}
fn describe_node(node: &Node) -> String {
let name = if node.name.is_empty() {
"<unnamed>"
} else {
node.name.as_str()
};
format!("{name}({})", node.op_type)
}
#[cfg(test)]
mod gather_tests {
use super::*;
use onnx_runtime_ir::static_shape;
#[test]
fn shape_capacity_is_not_assumed_when_the_result_is_observed() {
let mut graph = Graph::new();
graph.opset_imports.insert(String::new(), 17);
let input = graph.create_named_value("mask", DataType::Int64, static_shape([1, 8]));
graph.add_input(input);
let shape = graph.create_named_value("shape", DataType::Int64, static_shape([2]));
graph.insert_node(Node::new(
NodeId(0),
"Shape",
vec![Some(input)],
vec![shape],
));
assert!(!binding_shape_result_is_observed(&graph, input));
let observed = graph.create_named_value("observed", DataType::Int64, static_shape([2]));
graph.insert_node(Node::new(
NodeId(1),
"Identity",
vec![Some(shape)],
vec![observed],
));
graph.add_output(observed);
assert!(binding_shape_result_is_observed(&graph, input));
}
fn reference(
src: &[u8],
shape: &[usize],
strides: &[i64],
byte_offset: usize,
esize: usize,
) -> Vec<u8> {
let n: usize = shape.iter().product();
let mut out = vec![0u8; n * esize];
if n == 0 {
return out;
}
let rank = shape.len();
let mut idx = vec![0usize; rank];
let mut w = 0usize;
loop {
let mut off = byte_offset as i64;
for d in 0..rank {
off += strides[d] * idx[d] as i64 * esize as i64;
}
let s = off as usize;
out[w..w + esize].copy_from_slice(&src[s..s + esize]);
w += esize;
let mut carried = true;
for axis in (0..rank).rev() {
idx[axis] += 1;
if idx[axis] < shape[axis] {
carried = false;
break;
}
idx[axis] = 0;
}
if carried {
break;
}
}
out
}
fn contiguous(shape: &[usize]) -> Vec<i64> {
let mut strides = vec![1i64; shape.len()];
for axis in (0..shape.len().saturating_sub(1)).rev() {
strides[axis] = strides[axis + 1] * shape[axis + 1] as i64;
}
strides
}
fn permuted(base: &[usize], perm: &[usize]) -> (Vec<usize>, Vec<i64>) {
let strides = contiguous(base);
(
perm.iter().map(|&p| base[p]).collect(),
perm.iter().map(|&p| strides[p]).collect(),
)
}
fn permutations(rank: usize) -> Vec<Vec<usize>> {
fn go(current: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
if k == current.len() {
out.push(current.clone());
return;
}
for i in k..current.len() {
current.swap(k, i);
go(current, k + 1, out);
current.swap(k, i);
}
}
let mut result = Vec::new();
let mut current: Vec<usize> = (0..rank).collect();
go(&mut current, 0, &mut result);
result
}
#[test]
fn every_gather_path_matches_the_element_walk() {
let bases: &[&[usize]] = &[
&[6],
&[5, 3],
&[1, 7],
&[7, 1],
&[2, 3, 4],
&[1, 6, 5],
&[6, 1, 5],
&[6, 5, 1],
&[2, 3, 4, 5],
&[2, 1, 4, 5],
&[1, 3, 1, 5],
&[2, 8, 3, 16],
];
for base in bases {
let n: usize = base.iter().product();
let src: Vec<u8> = (0..n * 4).map(|i| (i * 31 % 251) as u8).collect();
for perm in permutations(base.len()) {
let (shape, strides) = permuted(base, &perm);
assert_eq!(
gather_view(&src, &shape, &strides, 0, 4),
reference(&src, &shape, &strides, 0, 4),
"base {base:?} perm {perm:?}"
);
}
}
}
#[test]
fn a_byte_offset_shifts_the_whole_gather() {
let base = [4usize, 6];
let src: Vec<u8> = (0..24 * 4 + 64).map(|i| (i % 241) as u8).collect();
let (shape, strides) = permuted(&base, &[1, 0]);
for byte_offset in [0usize, 4, 40, 64] {
assert_eq!(
gather_view(&src, &shape, &strides, byte_offset, 4),
reference(&src, &shape, &strides, byte_offset, 4),
"byte_offset={byte_offset}"
);
}
}
#[test]
fn negative_strides_are_gathered_in_the_right_order() {
let src: Vec<u8> = (0..5 * 4 * 4).map(|i| (i % 251) as u8).collect();
let shape = [5usize, 4];
let strides = [-4i64, 1];
let byte_offset = 4 * 4 * 4;
assert_eq!(
gather_view(&src, &shape, &strides, byte_offset, 4),
reference(&src, &shape, &strides, byte_offset, 4)
);
}
#[test]
fn a_zero_stride_axis_repeats_rather_than_advances() {
let src: Vec<u8> = (0..4 * 4).map(|i| (i % 251) as u8).collect();
let shape = [3usize, 4];
let strides = [0i64, 1];
assert_eq!(
gather_view(&src, &shape, &strides, 0, 4),
reference(&src, &shape, &strides, 0, 4)
);
}
#[test]
fn collapse_reduces_a_contiguous_view_to_one_axis() {
assert_eq!(
collapse_view(&[2, 3, 4], &contiguous(&[2, 3, 4])),
(vec![24], vec![1])
);
assert_eq!(collapse_view(&[1, 5, 1], &[7, 1, 3]), (vec![5], vec![1]));
assert_eq!(collapse_view(&[1, 1, 1], &[9, 9, 9]), (vec![1], vec![0]));
let (shape, strides) = permuted(&[2, 8, 4, 64], &[0, 2, 1, 3]);
assert_eq!(
collapse_view(&shape, &strides),
(vec![2, 4, 8, 64], vec![2048, 64, 256, 1])
);
}
#[test]
fn the_parallel_gather_is_bit_identical_to_the_serial_one() {
let base = [1usize, 257, 12, 64];
let n: usize = base.iter().product();
let src: Vec<u8> = (0..n * 4).map(|i| (i * 17 % 251) as u8).collect();
let (shape, strides) = permuted(&base, &[0, 2, 1, 3]);
let expect = reference(&src, &shape, &strides, 0, 4);
for threads in [1usize, 3, 8] {
let got = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
.install(|| gather_view(&src, &shape, &strides, 0, 4));
assert_eq!(got, expect, "threads={threads}");
}
}
#[test]
fn an_empty_view_gathers_to_an_empty_buffer() {
assert!(gather_view(&[], &[0, 4], &[4, 1], 0, 4).is_empty());
assert!(gather_view(&[1, 2, 3, 4], &[3, 0], &[1, 1], 0, 4).is_empty());
}
}