diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4034936
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,6 @@
+- It's more important that this done right than that this can be done quickly, and you can stop and ask me things if necessary.
+- Add as few additional functions or data structures as possible to achieve your goals.
+- No layer violations. If a change would require moving things between compiler layers or mixing behaviour between layers, ask before making it.
+- Unit tests must be randomized and property-based, and not just test exact equivalence to some value.
+- If you think there's a "cleaner final boundary" for a feature or change, surface it.
+- If making a specified refactor, check all files using the old version to check that the refactor is done completely.
diff --git a/Cargo.lock b/Cargo.lock
index ad3c406..ee333dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -329,6 +329,7 @@ dependencies = [
"ipu-package",
"ipu-profile",
"ipu-runtime",
+ "ipu-target",
"serde_json",
"tracing",
"tracing-subscriber",
@@ -342,8 +343,8 @@ dependencies = [
"foldhash",
"ipu-driver",
"ipu-elf",
- "ipu-exchange",
"ipu-package",
+ "ipu-target",
"rayon",
"serde",
"thiserror",
@@ -356,6 +357,7 @@ version = "0.1.0"
dependencies = [
"fastrand",
"ipu-package",
+ "ipu-target",
"libc",
"object",
"thiserror",
@@ -375,16 +377,6 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "ipu-exchange"
-version = "0.1.0"
-dependencies = [
- "fastrand",
- "serde",
- "thiserror",
- "tracing",
-]
-
[[package]]
name = "ipu-package"
version = "0.1.0"
@@ -392,6 +384,7 @@ dependencies = [
"capnp",
"capnpc",
"fastrand",
+ "ipu-target",
"thiserror",
"tracing",
]
@@ -411,10 +404,19 @@ version = "0.1.0"
dependencies = [
"ipu-driver",
"ipu-package",
- "thiserror",
"tracing-subscriber",
]
+[[package]]
+name = "ipu-target"
+version = "0.1.0"
+dependencies = [
+ "fastrand",
+ "serde",
+ "thiserror",
+ "tracing",
+]
+
[[package]]
name = "ipu-tests"
version = "0.1.0"
@@ -427,9 +429,9 @@ dependencies = [
"ipu-codegen",
"ipu-driver",
"ipu-elf",
- "ipu-exchange",
"ipu-package",
"ipu-runtime",
+ "ipu-target",
"rand_distr",
"rand_xoshiro",
"serde_json",
diff --git a/Cargo.toml b/Cargo.toml
index 4511359..f3a8abf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[workspace]
members = [
- "crates/ipu-exchange",
+ "crates/ipu-target",
"crates/ipu-codegen",
"crates/ipu-package",
"crates/ipu-elf",
diff --git a/README.md b/README.md
index be10d7e..f2ea2f0 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,15 @@
# ipu-stack
-`ipu-stack` is a small collection of runtime, packaging, code-generation, and
-early graph-lowering components for Graphcore IPU21 devices. It does not yet
-contain the allocator or complete graph-to-device compiler.
+`ipu-stack` is a collection of graph lowering, code generation, packaging, and
+runtime components for Graphcore IPU21 devices.
## Components
-- `ipu-exchange` generates and encodes device and host exchange programs.
-- `ipu-codegen` emits straight-line supervisor code from caller-resolved tile
- programs.
+- `ipu-target` owns IPU21 topology and memory geometry, exchange generation and
+ parsing, instruction encoding, finalized tile programs, and supervisor-code
+ emission.
+- `ipu-codegen` plans and lowers graphs and builds loadable packages using that
+ target API.
- `ipu-elf` compiles Graphcore tile sources and links Colossus ELF objects.
- `ipu-package` reads, writes, and validates `.ipuexe` application packages and
cycle profiles.
@@ -30,8 +31,8 @@ graph is shaped structured SSA. Its separate mid-level lowering selects
precision and layout with a toy cost model and inserts explicit casts and
rearrangements. Mid-to-low lowering then produces logical per-tile shard work,
kernel runs, synchronized exchanges, and structured repeats. Package
-construction produces completion-only tile programs until SRAM placement,
-exchange encoding, and kernel-symbol selection are implemented. The config
+construction resolves SRAM placement, exchange encoding, and kernel symbols
+before `ipu-target` emits each finalized tile program. The config
uses one shared `PipelineConfig` for target, tile count, input formats, operator
catalog, scheduling, and profiling. `PackageConfig` adds the toolchain, static
runtime source, and build directory.
* Unmerged path TODO
diff --git a/crates/ipu-cli/Cargo.toml b/crates/ipu-cli/Cargo.toml
index 32617f1..0c4e673 100644
--- a/crates/ipu-cli/Cargo.toml
+++ b/crates/ipu-cli/Cargo.toml
@@ -18,6 +18,7 @@ ipu-elf = { path = "../ipu-elf" }
ipu-package = { path = "../ipu-package" }
ipu-profile = { path = "../ipu-profile" }
ipu-runtime = { path = "../ipu-runtime" }
+ipu-target = { path = "../ipu-target" }
serde_json.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
* Unmerged path crates/ipu-cli/src/main.rs
diff --git a/crates/ipu-cli/src/profile_report.html b/crates/ipu-cli/src/profile_report.html
index 9e70b39..80eb080 100644
--- a/crates/ipu-cli/src/profile_report.html
+++ b/crates/ipu-cli/src/profile_report.html
@@ -158,7 +158,7 @@ canvas {
tx
rx
tx/rx
- partner bus
+ partner bus
exchange idle
sync
@@ -225,7 +225,7 @@ const colors = {
send: "#17854c",
receive: "#287db2",
sendReceive: "#6b5596",
- partnerBusy: "#9a547f",
+ partnerBusy: "#ffa756",
exchangeIdle: "#8ebeb4",
compute: "#cf3f4a",
synchronization: "#c58b24",
diff --git a/crates/ipu-codegen/Cargo.toml b/crates/ipu-codegen/Cargo.toml
index dfa72f0..5f86789 100644
--- a/crates/ipu-codegen/Cargo.toml
+++ b/crates/ipu-codegen/Cargo.toml
@@ -6,7 +6,7 @@ license.workspace = true
[dependencies]
foldhash.workspace = true
-ipu-exchange = { path = "../ipu-exchange" }
+ipu-target = { path = "../ipu-target" }
ipu-driver = { path = "../ipu-driver" }
ipu-elf = { path = "../ipu-elf" }
ipu-package = { path = "../ipu-package" }
diff --git a/crates/ipu-codegen/src/config.rs b/crates/ipu-codegen/src/config.rs
new file mode 100644
index 0000000..9a0ca01
--- /dev/null
+++ b/crates/ipu-codegen/src/config.rs
@@ -0,0 +1,341 @@
+//! Compiler target and planner search configuration.
+
+use crate::graph::{TensorShape, ValueId};
+use crate::layout::{MemoryClass, TensorFormat};
+use crate::operator::{GemmPlanConstraint, Precision};
+use ipu_target::hardware::HardwareTarget;
+use std::collections::BTreeMap;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct PipelineConfig {
+ pub target: HardwareTarget,
+ pub tile_count: u16,
+ pub inputs: BTreeMap,
+ /// Graph-boundary tensors whose layout may be selected by their first
+ /// consumer. Precision remains fixed, while packaging exposes the chosen
+ /// physical layout directly through the host binding.
+ pub automatic_inputs: BTreeMap,
+ pub search_domain: PlannerSearchDomain,
+ pub planning_beam_width: usize,
+ /// Complete beam finalists ranked with the physical exchange scheduler.
+ pub exchange_schedule_finalists: usize,
+ /// SRAM retained for exchange tables, profiling, host commands, and code.
+ pub standard_memory_reservation_bytes: u64,
+ /// Maximum SRAM per tile available to planned values and the reservation.
+ pub tile_memory_budget_bytes: u64,
+ pub profiling: ProfilingConfig,
+ pub diagnostic_checkpoints: bool,
+ pub conversion_streaming: ConversionStreamingPolicy,
+ pub(crate) resolved_active_tile_counts: Vec,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum ConversionStreamingPolicy {
+ Never,
+ #[default]
+ WhenRequired,
+ Always,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum AttentionStrategy {
+ #[default]
+ Automatic,
+ Flash,
+ Materialized,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum OperatorClass {
+ Gemm,
+ Gelu,
+ Add,
+ Attention,
+}
+
+/// Search axes shared by semantic operator plan generators.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct PlannerSearchDomain {
+ /// `None` derives useful tile counts from graph shapes; `Some` restricts
+ /// planning to the explicitly supplied counts.
+ pub(crate) active_tiles: Option>,
+ pub(crate) operator_precisions: BTreeMap>,
+ pub(crate) weight_memory_classes: Vec,
+ pub(crate) attention_strategy: AttentionStrategy,
+ pub(crate) gemm_plan_constraints: Vec,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum ProfilingConfig {
+ #[default]
+ Disabled,
+ Overall,
+ Full,
+}
+
+impl std::fmt::Display for AttentionStrategy {
+ fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ output.write_str(match self {
+ Self::Automatic => "auto",
+ Self::Flash => "flash",
+ Self::Materialized => "materialized",
+ })
+ }
+}
+
+impl std::str::FromStr for AttentionStrategy {
+ type Err = &'static str;
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "auto" | "automatic" => Ok(Self::Automatic),
+ "flash" => Ok(Self::Flash),
+ "materialized" => Ok(Self::Materialized),
+ _ => Err("expected auto, flash, or materialized"),
+ }
+ }
+}
+
+impl Default for PlannerSearchDomain {
+ fn default() -> Self {
+ Self {
+ active_tiles: None,
+ operator_precisions: BTreeMap::from([
+ (OperatorClass::Gemm, vec![Precision::F16, Precision::F32]),
+ (OperatorClass::Gelu, vec![Precision::F16, Precision::F32]),
+ (OperatorClass::Add, vec![Precision::F16, Precision::F32]),
+ (OperatorClass::Attention, vec![Precision::F16]),
+ ]),
+ weight_memory_classes: vec![MemoryClass::Standard, MemoryClass::Interleaved],
+ attention_strategy: AttentionStrategy::Automatic,
+ gemm_plan_constraints: Vec::new(),
+ }
+ }
+}
+
+impl PlannerSearchDomain {
+ pub(crate) fn precisions(&self, operator: OperatorClass) -> &[Precision] {
+ self.operator_precisions
+ .get(&operator)
+ .map_or(&[], Vec::as_slice)
+ }
+
+ pub(crate) fn permits_precision(&self, operator: OperatorClass, precision: Precision) -> bool {
+ self.precisions(operator).contains(&precision)
+ }
+
+ pub(crate) fn permits_weight_memory(&self, memory_class: MemoryClass) -> bool {
+ self.weight_memory_classes.contains(&memory_class)
+ }
+
+ pub(crate) fn active_tile_counts<'a>(
+ &self,
+ capacity: u16,
+ shapes: impl IntoIterator- ,
+ ) -> Vec {
+ match &self.active_tiles {
+ None => {
+ let mut counts = candidate_active_tile_counts(capacity);
+ for count in shape_aware_active_tile_counts(capacity, shapes) {
+ if !counts.contains(&count) {
+ counts.push(count);
+ }
+ }
+ counts
+ }
+ Some(counts) => counts
+ .iter()
+ .copied()
+ .filter(|&count| count <= capacity)
+ .collect(),
+ }
+ }
+
+ pub fn with_operator_precisions(
+ mut self,
+ operator: OperatorClass,
+ precisions: impl IntoIterator
- ,
+ ) -> Self {
+ let mut unique = Vec::new();
+ for precision in precisions {
+ if !unique.contains(&precision) {
+ unique.push(precision);
+ }
+ }
+ self.operator_precisions.insert(operator, unique);
+ self
+ }
+
+ pub fn with_weight_memory_classes(
+ mut self,
+ classes: impl IntoIterator
- ,
+ ) -> Self {
+ let mut unique = Vec::new();
+ for class in classes {
+ if !unique.contains(&class) {
+ unique.push(class);
+ }
+ }
+ self.weight_memory_classes = unique;
+ self
+ }
+
+ pub fn with_active_tile_counts(mut self, counts: impl IntoIterator
- ) -> Self {
+ let mut active_tiles = Vec::new();
+ for count in counts {
+ if count != 0 && !active_tiles.contains(&count) {
+ active_tiles.push(count);
+ }
+ }
+ self.active_tiles = Some(active_tiles);
+ self
+ }
+
+ pub fn with_attention_strategy(mut self, strategy: AttentionStrategy) -> Self {
+ self.attention_strategy = strategy;
+ self
+ }
+
+ pub fn with_gemm_plan_constraint(mut self, constraint: GemmPlanConstraint) -> Self {
+ self.gemm_plan_constraints
+ .retain(|existing| existing.source_operation != constraint.source_operation);
+ self.gemm_plan_constraints.push(constraint);
+ self
+ }
+}
+
+impl ProfilingConfig {
+ pub const fn records_overall_time(self) -> bool {
+ !matches!(self, Self::Disabled)
+ }
+
+ pub const fn records_steps(self) -> bool {
+ matches!(self, Self::Full)
+ }
+}
+
+impl std::fmt::Display for ProfilingConfig {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter.write_str(match self {
+ Self::Disabled => "none",
+ Self::Overall => "overall",
+ Self::Full => "full",
+ })
+ }
+}
+
+impl std::str::FromStr for ProfilingConfig {
+ type Err = &'static str;
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "none" | "disabled" => Ok(Self::Disabled),
+ "overall" => Ok(Self::Overall),
+ "full" => Ok(Self::Full),
+ _ => Err("profiling mode must be one of: none, overall, full"),
+ }
+ }
+}
+
+impl PipelineConfig {
+ pub fn new(tile_count: u16) -> Self {
+ let target = HardwareTarget::Ipu21;
+ let memory = target.memory_constraints();
+ Self {
+ target,
+ tile_count,
+ inputs: BTreeMap::new(),
+ automatic_inputs: BTreeMap::new(),
+ search_domain: PlannerSearchDomain::default(),
+ planning_beam_width: 64,
+ exchange_schedule_finalists: 1,
+ standard_memory_reservation_bytes: memory.default_standard_reservation_bytes,
+ tile_memory_budget_bytes: memory.total_bytes,
+ profiling: ProfilingConfig::default(),
+ diagnostic_checkpoints: false,
+ conversion_streaming: ConversionStreamingPolicy::WhenRequired,
+ resolved_active_tile_counts: Vec::new(),
+ }
+ }
+
+ pub fn with_input(mut self, value: ValueId, format: TensorFormat) -> Self {
+ self.inputs.insert(value, format);
+ self.automatic_inputs.remove(&value);
+ self
+ }
+
+ pub fn with_automatic_input(mut self, value: ValueId, precision: Precision) -> Self {
+ self.inputs.remove(&value);
+ self.automatic_inputs.insert(value, precision);
+ self
+ }
+
+ pub fn with_planning_beam_width(mut self, width: usize) -> Self {
+ self.planning_beam_width = width.max(1);
+ self
+ }
+
+ pub fn with_exchange_schedule_finalists(mut self, finalists: usize) -> Self {
+ self.exchange_schedule_finalists = finalists.max(1);
+ self
+ }
+
+ pub fn with_search_domain(mut self, search_domain: PlannerSearchDomain) -> Self {
+ self.search_domain = search_domain;
+ self
+ }
+
+ pub fn with_standard_memory_reservation(mut self, bytes: u64) -> Self {
+ self.standard_memory_reservation_bytes = bytes;
+ self
+ }
+
+ pub fn with_tile_memory_budget(mut self, bytes: u64) -> Self {
+ self.tile_memory_budget_bytes = bytes;
+ self
+ }
+}
+
+pub(crate) fn candidate_active_tile_counts(capacity: u16) -> Vec {
+ if capacity == 0 {
+ return vec![0];
+ }
+ let mut counts = vec![capacity];
+ // Power-of-two subsets provide progressively smaller fallback grids.
+ let mut power = 1u16;
+ while let Some(next) = power.checked_mul(2) {
+ if next > capacity {
+ break;
+ }
+ power = next;
+ }
+ loop {
+ if !counts.contains(&power) {
+ counts.push(power);
+ }
+ if power == 1 {
+ break;
+ }
+ power /= 2;
+ }
+ counts
+}
+
+pub(crate) fn shape_aware_active_tile_counts<'a>(
+ capacity: u16,
+ shapes: impl IntoIterator
- ,
+) -> Vec {
+ let minimum = capacity.div_ceil(2);
+ let mut counts = shapes
+ .into_iter()
+ .flat_map(|shape| shape.0.iter().copied())
+ .filter_map(|extent| {
+ let extent = u16::try_from(extent).ok()?;
+ (extent > 1 && extent <= capacity).then(|| capacity / extent * extent)
+ })
+ .filter(|&count| count >= minimum && count < capacity)
+ .collect::>();
+ counts.sort_unstable_by(|left, right| right.cmp(left));
+ counts.dedup();
+ counts
+}
diff --git a/crates/ipu-codegen/src/conversion.rs b/crates/ipu-codegen/src/conversion.rs
new file mode 100644
index 0000000..57d1add
--- /dev/null
+++ b/crates/ipu-codegen/src/conversion.rs
@@ -0,0 +1,1244 @@
+//! Address-independent layout-conversion routes and copy geometry.
+
+use crate::graph::TensorShape;
+use crate::layout::{
+ AMP_COLUMN_MICRO, Layout, LayoutError, NativeKernelOrder, StorageOrder, TensorRegion,
+ TensorType,
+};
+use crate::mid::{MidGraph, MidOperation, MidOperationKind, MidValue};
+use crate::operator::Precision;
+use crate::storage::{StorageError, amp_micro_dimension, physical_byte_offset};
+use std::collections::BTreeMap;
+
+/// Address-independent strategy for materializing a format conversion.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ConversionStrategy {
+ LocalKernel,
+ DirectRetile,
+ DirectLogical,
+ StageLogicalThenTransform,
+}
+
+impl ConversionStrategy {
+ pub const fn uses_intersections(self) -> bool {
+ !matches!(self, Self::LocalKernel)
+ }
+}
+
+pub fn layout_conversion_strategy(
+ precision: Precision,
+ from: &Layout,
+ to: &Layout,
+) -> ConversionStrategy {
+ if from.order == to.order {
+ ConversionStrategy::DirectRetile
+ } else if precision == Precision::F32
+ || matches!(from.order, StorageOrder::Linear)
+ && matches!(
+ to.order,
+ StorageOrder::Native(NativeKernelOrder::Left | NativeKernelOrder::Output)
+ )
+ || matches!(to.order, StorageOrder::Linear)
+ || matches!(
+ (from.order, to.order),
+ (StorageOrder::Blocked(source), StorageOrder::Blocked(destination))
+ if source.axes == destination.axes
+ && source.permutation == destination.permutation
+ )
+ {
+ ConversionStrategy::DirectLogical
+ } else {
+ ConversionStrategy::StageLogicalThenTransform
+ }
+}
+
+/// One arbitrary-rank affine copy nest within a conversion route.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct CopyGeometry {
+ pub source_offset: u32,
+ pub destination_offset: u32,
+ pub contiguous_bytes: u32,
+ /// Inner to outer affine dimensions.
+ pub dimensions: Vec,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct CopyDimension {
+ pub count: u32,
+ pub source_stride: u32,
+ pub destination_stride: u32,
+}
+
+impl CopyGeometry {
+ pub fn copy_count(&self) -> u64 {
+ self.dimensions.iter().fold(1, |copies, dimension| {
+ copies.saturating_mul(u64::from(dimension.count))
+ })
+ }
+
+ pub fn bytes(&self) -> u64 {
+ u64::from(self.contiguous_bytes).saturating_mul(self.copy_count())
+ }
+
+ pub(crate) fn offsets(&self) -> Option> {
+ let mut offsets = vec![(self.source_offset, self.destination_offset)];
+ for dimension in &self.dimensions {
+ let inner = offsets.clone();
+ offsets.clear();
+ offsets.reserve(inner.len().checked_mul(dimension.count as usize)?);
+ for index in 0..dimension.count {
+ let source_delta = index.checked_mul(dimension.source_stride)?;
+ let destination_delta = index.checked_mul(dimension.destination_stride)?;
+ for &(source, destination) in &inner {
+ offsets.push((
+ source.checked_add(source_delta)?,
+ destination.checked_add(destination_delta)?,
+ ));
+ }
+ }
+ }
+ Some(offsets)
+ }
+}
+
+/// A semantic intersection between one resolved source shard and one
+/// resolved destination shard. Regions remain address-independent; low
+/// lowering binds them to the corresponding shard IDs.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ConversionMapping {
+ pub source_shard: u32,
+ pub source_tile: u16,
+ pub source_storage: TensorRegion,
+ pub destination_shard: u32,
+ pub destination_tile: u16,
+ pub destination_storage: TensorRegion,
+ pub source_region: TensorRegion,
+ pub destination_region: TensorRegion,
+ /// Local copies from the source layout into word-aligned transfer staging.
+ pub source_copies: Vec,
+ pub copies: Vec,
+ /// Local copies from word-aligned transfer staging into the planned
+ /// destination storage.
+ pub destination_copies: Vec,
+}
+
+/// A logical view whose physical materialization may be deferred until a
+/// consumer requests bounded slices.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum DeferredTransform {
+ /// Split the final input axis into `parts` equal-width slices and fold the
+ /// new part axis into the leading output axis.
+ SplitLastAxisIntoLeading { parts: u32 },
+}
+
+impl DeferredTransform {
+ pub fn map_slices(
+ self,
+ source_shape: &TensorShape,
+ output_shape: &TensorShape,
+ output: &TensorRegion,
+ ) -> Option> {
+ let streams = output.extents.first()?;
+ (streams.start..streams.logical_end)
+ .map(|stream| {
+ let mut slice = output.clone();
+ slice.extents[0].start = stream;
+ slice.extents[0].logical_end = stream.checked_add(1)?;
+ slice.extents[0].physical_end = stream.checked_add(1)?;
+ self.map_slice(source_shape, output_shape, &slice)
+ })
+ .collect()
+ }
+
+ pub(crate) fn map_slice(
+ self,
+ source_shape: &TensorShape,
+ output_shape: &TensorShape,
+ output: &TensorRegion,
+ ) -> Option {
+ let Self::SplitLastAxisIntoLeading { parts } = self;
+ let [source_batch, source_rows, source_columns] = source_shape.0.as_slice() else {
+ return None;
+ };
+ let [output_streams, output_rows, output_columns] = output_shape.0.as_slice() else {
+ return None;
+ };
+ let [stream, rows, columns] = output.extents.as_slice() else {
+ return None;
+ };
+ if parts == 0
+ || stream.axis != 0
+ || rows.axis != 1
+ || columns.axis != 2
+ || stream.logical_end != stream.start.checked_add(1)?
+ || *output_streams != source_batch.checked_mul(parts)?
+ || output_rows != source_rows
+ || source_columns != &output_columns.checked_mul(parts)?
+ || stream.logical_end > *output_streams
+ || rows.logical_end > *output_rows
+ || columns.logical_end > *output_columns
+ {
+ return None;
+ }
+ let batch = stream.start / parts;
+ let column_base = (stream.start % parts).checked_mul(*output_columns)?;
+ Some(DeferredSliceMapping {
+ source: TensorRegion::new([
+ crate::ShardExtent {
+ axis: 0,
+ start: batch,
+ logical_end: batch.checked_add(1)?,
+ physical_end: batch.checked_add(1)?,
+ },
+ *rows,
+ crate::ShardExtent {
+ axis: 2,
+ start: column_base.checked_add(columns.start)?,
+ logical_end: column_base.checked_add(columns.logical_end)?,
+ physical_end: column_base.checked_add(columns.logical_end)?,
+ },
+ ]),
+ destination: output.clone(),
+ source_axes: vec![None, Some(1), Some(2)],
+ })
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct DeferredSliceMapping {
+ pub source: TensorRegion,
+ pub destination: TensorRegion,
+ /// Source axis corresponding to each destination axis. `None` selects a
+ /// fixed source coordinate.
+ pub source_axes: Vec