1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::{
INLINE_DIMS, MetadataError,
shape::Shape,
strides::Strides,
tiling::{MAX_FRAGMENTS, Tiling},
};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
pub struct Metadata {
pub shape: Shape,
pub strides: Strides,
/// How many fragments each logical dim is stored as; untiled by default.
/// The shape and strides stay physical. See [`Tiling`].
pub tiling: Tiling,
}
impl Metadata {
pub fn new(shape: impl Into<Shape>, strides: impl Into<Strides>) -> Self {
let shape = shape.into();
let strides = strides.into();
debug_assert_eq!(
shape.rank(),
strides.rank(),
"Rank of shape and strides must be the same"
);
Self {
shape,
strides,
tiling: Tiling::UNTILED,
}
}
/// This metadata with `tiling` labelling its physical dims.
///
/// # Errors
///
/// When `tiling` does not describe this rank: see [`Tiling::new`].
pub fn with_tiling(mut self, tiling: Tiling) -> Result<Self, MetadataError> {
tiling.logical_rank(self.rank())?;
self.tiling = tiling;
Ok(self)
}
/// Whether any logical dim is stored as more than one fragment.
pub fn is_tiled(&self) -> bool {
self.tiling.is_tiled()
}
/// How many dims this buffer stands for: its rank, less the extra fragments
/// the tiling splits dims into.
///
/// # Errors
///
/// When the tiling does not fit this rank: see [`Tiling::logical_rank`].
pub fn logical_rank(&self) -> Result<usize, MetadataError> {
self.tiling.logical_rank(self.rank())
}
/// The extents this buffer stands for: each logical dim's fragments
/// multiplied back together, in logical order. An untiled metadata gives its
/// shape unchanged.
///
/// # Errors
///
/// When the tiling does not fit this rank: see [`Tiling::logical_rank`].
pub fn logical_shape(&self) -> Result<Shape, MetadataError> {
let fragments = self.tiling.fragments(self.logical_rank()?);
let mut extents = SmallVec::<[usize; INLINE_DIMS]>::from_elem(1, fragments.len());
// A dim's fragments are spread through the buffer rather than adjacent,
// so walk the levels in the order the storage was laid out, coarsest
// first, and each dim collects its own.
let dims = self.shape.as_slice();
let mut physical = 0;
for level in 0..MAX_FRAGMENTS {
for (dim, &count) in fragments.iter().enumerate() {
if level < count {
extents[dim] *= dims[physical];
physical += 1;
}
}
}
Ok(Shape::new_raw(extents))
}
/// The dim-changing ops do not carry a tiling yet: they refuse rather than
/// return counts over dims that moved.
fn assert_untiled(&self, op: &str) {
assert!(
!self.is_tiled(),
"Metadata::{op} on a storage-tiled tensor is not supported: {:?}",
self.tiling
);
}
pub fn shape(&self) -> &Shape {
&self.shape
}
/// The shape, to rewrite in place. A storage-tiled tensor's dims are its tiling's fragments,
/// which a rewrite would leave stale, so it refuses like the dim-changing ops do.
pub fn shape_mut(&mut self) -> &mut Shape {
self.assert_untiled("shape_mut");
&mut self.shape
}
pub fn strides(&self) -> &Strides {
&self.strides
}
/// The strides, to rewrite in place. Refuses on a storage-tiled tensor, as
/// [`shape_mut`](Self::shape_mut) does: its strides step its tiles, and a rewrite would
/// keep the tiling over strides that no longer do.
pub fn strides_mut(&mut self) -> &mut Strides {
self.assert_untiled("strides_mut");
&mut self.strides
}
pub fn rank(&self) -> usize {
self.num_dims()
}
pub fn num_dims(&self) -> usize {
self.shape.num_dims()
}
/// Returns the total number of elements of a tensor having this shape
pub fn num_elements(&self) -> usize {
self.shape.num_elements()
}
pub fn swapped(mut self, dim0: usize, dim1: usize) -> Self {
self.swap(dim0, dim1);
self
}
pub fn swap(&mut self, dim0: usize, dim1: usize) {
self.assert_untiled("swap");
debug_assert!(dim0 < self.rank(), "dim0 is out of bounds");
debug_assert!(dim1 < self.rank(), "dim1 is out of bounds");
self.shape.swap(dim0, dim1);
self.strides.swap(dim0, dim1);
}
/// Reorder the shape dimensions according to the permutation of `axes`.
pub fn permute(&mut self, axes: &[usize]) -> Result<(), MetadataError> {
self.assert_untiled("permute");
self.shape.permute(axes)?;
self.strides.permute(axes)?;
Ok(())
}
pub fn permuted(mut self, axes: &[usize]) -> Result<Self, MetadataError> {
self.permute(axes)?;
Ok(self)
}
/// Insert a dimension of `shape` with `stride` at position `index`.
pub fn insert(&mut self, index: usize, shape: usize, stride: usize) {
self.assert_untiled("insert");
self.shape.insert(index, shape);
self.strides.insert(index, stride);
}
/// Remove and return the dimension at position `index` from the metadata.
pub fn remove(&mut self, index: usize) -> (usize, usize) {
self.assert_untiled("remove");
let shape = self.shape.remove(index);
let stride = self.strides.remove(index);
(shape, stride)
}
/// Appends a dimension of `shape` with `stride` to the back of the metadata.
pub fn push(&mut self, shape: usize, stride: usize) {
self.assert_untiled("push");
self.shape.push(shape);
self.strides.push(stride);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A `[b, m, k]` operand stored `[Bs, Mx, Ky, Mi, Kj]`: the two tiled dims
/// collect a fragment from each level, the untiled one drops out after the
/// first.
#[test]
fn logical_shape_multiplies_a_dim_s_fragments_back_together() {
let meta = Metadata::new(
[2, 128, 344, 32, 32],
[128 * 344 * 1024, 344 * 1024, 1024, 32, 1],
)
.with_tiling(Tiling::new(&[1, 2, 2]).unwrap())
.unwrap();
assert_eq!(meta.logical_rank(), Ok(3));
assert_eq!(meta.logical_shape(), Ok(Shape::new([2, 4096, 11008])));
}
/// The untiled case is the identity, so a caller reasoning in logical dims
/// need not ask whether the tensor is tiled first.
#[test]
fn an_untiled_metadata_stands_for_its_own_shape() {
let meta = Metadata::new([2, 4096, 11008], [4096 * 11008, 11008, 1]);
assert_eq!(meta.logical_rank(), Ok(3));
assert_eq!(meta.logical_shape(), Ok(meta.shape.clone()));
}
/// Three levels deep on one dim, one on another: the fragment counts need
/// not match, and the physical order stays level-major.
#[test]
fn dims_tiled_to_different_depths_each_collect_their_own() {
let meta = Metadata::new([4, 8, 2, 4, 2], [1; 5])
.with_tiling(Tiling::new(&[3, 2]).unwrap())
.unwrap();
assert_eq!(meta.logical_shape(), Ok(Shape::new([4 * 2 * 2, 8 * 4])));
}
/// A buffer too short to hold the fragments is the caller pairing the wrong
/// tensor with the wrong description, and says so rather than guessing.
#[test]
fn a_buffer_too_short_for_the_tiling_is_refused() {
let mut meta = Metadata::new([2, 128, 344, 32, 32], [1; 5])
.with_tiling(Tiling::new(&[1, 2, 2]).unwrap())
.unwrap();
meta.shape.remove(4);
meta.strides.remove(4);
meta.shape.remove(3);
meta.strides.remove(3);
assert!(meta.logical_rank().is_err());
assert!(meta.logical_shape().is_err());
}
}