1 import Base
2 import ../shape/core.bend as Shape
3 import ./core.bend as Core
4 import ./layout.bend as Layout
5 import ./transforms.bend as Transforms
6 import ./transpose_spec.bend as Transpose
7 import ./slice.bend as Slice
8
9 # PyTorch's signed axes, indices and reshape sizes, parsed into the unsigned
10 # ones that the tensor operations check. A Position counts from either end, as
11 # a PyTorch int does: First{k} is k, and Last{k} is -1 - k, so Last{0} is -1,
12 # the last axis or element. Each integer has exactly one Position.
13 type Position is Data:
14 First{k: U32}
15 Last{k: U32}
16
17 # A reshape size: an exact size, or -1, the one size PyTorch infers.
18 type Extent is Data:
19 Exact{size: U32}
20 Inferred{}
21
22 # A signed reshape's refusals: the unsigned reshape's, more than one Inferred
23 # size, and an Inferred size that any value would fit, on an empty input whose
24 # other sizes multiply to 0.
25 type Error is Data:
26 TransformError{error: Transforms.Error}
27 RepeatedInferred{}
28 AmbiguousInferred{}
29
30 # PyTorch's wrap of a position among `limit` axes or elements (maybe_wrap_dim):
31 # a position d in -limit..limit-1 becomes d + limit when d is negative and d
32 # otherwise. A position outside that range stays outside it, at or past
33 # `limit`, so the operation refuses it with its own check, code and order:
34 # First{k} stays k, and an out-of-range Last{k} becomes `limit`.
35 def wrap(+limit: U32, position: Position) -> U32:
36 match position:
37 case First{k}:
38 k
39 case Last{+k}:
40 Bool.pick(U32, U32.is_lt(k, limit), U32.sub(U32.sub(limit, 1), k), limit)
41
42 def wrap_all(+limit: U32, positions: List<&2, Position>) -> List<&2, U32>:
43 match positions:
44 case Nil{}:
45 Nil{}
46 case head <> tail:
47 wrap(limit, head) <> wrap_all(limit, tail)
48
49 # Each coordinate wrapped among its own axis's extent. A coordinate past the
50 # rank has no extent and is wrapped among 0: the read refuses the coordinate
51 # count before any range.
52 def wrap_each(dimensions: List<&2, U32>, positions: List<&2, Position>) -> List<&2, U32>:
53 match dimensions positions:
54 case Nil{} positions:
55 wrap_all(0, positions)
56 case extent <> rest Nil{}:
57 Nil{}
58 case extent <> rest head <> tail:
59 wrap(extent, head) <> wrap_each(rest, tail)
60
61 # Whether a position lies in PyTorch's range -limit..limit-1. Last{k} is
62 # -1 - k, at least -limit exactly when k < limit.
63 def inside(+limit: U32, position: Position) -> Bool:
64 match position:
65 case First{k}:
66 U32.is_lt(k, limit)
67 case Last{k}:
68 U32.is_lt(k, limit)
69
70 def all_inside(+limit: U32, positions: List<&2, Position>) -> Bool:
71 match positions:
72 case Nil{}:
73 True{}
74 case head <> tail:
75 Bool.and(inside(limit, head), all_inside(limit, tail))
76
77 # The axes of the input for transpose and sum: its rank, and 1 for a rank-0
78 # input, whose only axis is 0 (or -1).
79 def axis_limit(+input: Shape.Shape) -> U32:
80 U32.from_nat(Transpose.axis_limit(Shape.rank_dimensions(Shape.dimensions(input))))
81
82 # How many sizes are Inferred.
83 def inferred_count(extents: List<&2, Extent>) -> Nat:
84 match extents:
85 case Nil{}:
86 0n
87 case Exact{size} <> tail:
88 inferred_count(tail)
89 case Inferred{} <> tail:
90 1n+inferred_count(tail)
91
92 # The product of the exact sizes, in Nat: what the laws state the inference
93 # against. The inference never forms it, since it can pass the native Nat
94 # limit of 2^48 on an empty input: [-1, 2^24, 2^24] multiplies to 2^48.
95 def exact_product(extents: List<&2, Extent>) -> Nat:
96 match extents:
97 case Nil{}:
98 1n
99 case Exact{+size} <> tail:
100 Nat.mul(U32.to_nat(size), exact_product(tail))
101 case Inferred{} <> tail:
102 exact_product(tail)
103
104 # The sizes, each Inferred one replaced by `size`.
105 def filled(extents: List<&2, Extent>, +size: U32) -> List<&2, U32>:
106 match extents:
107 case Nil{}:
108 Nil{}
109 case Exact{extent} <> tail:
110 extent <> filled(tail, size)
111 case Inferred{} <> tail:
112 size <> filled(tail, size)
113
114 # The exact product against a bound, the input's element count: ZeroProduct
115 # when it is 0, Within{p} when it is p > 0 at most the bound, and Beyond when
116 # it is past the bound. capped only multiplies a size and a product that are
117 # both at most the bound, and a tensor holds at most 2^20 elements, so no
118 # number it forms passes 2^40.
119 type Product is Data:
120 ZeroProduct{}
121 Within{product: Nat}
122 Beyond{}
123
124 def bounded(product: Nat, below: Bool) -> Product:
125 match below:
126 case True{}:
127 Within{product}
128 case False{}:
129 Beyond{}
130
131 def cap(+bound: Nat, +product: Nat) -> Product:
132 bounded(product, Nat.is_le(product, bound))
133
134 def scaled(+bound: Nat, +size: Nat, +product: Nat, past: Bool) -> Product:
135 match past:
136 case True{}:
137 Beyond{}
138 case False{}:
139 cap(bound, Nat.mul(size, product))
140
141 # A nonzero size times the product of the sizes after it. A size past the
142 # bound makes any nonzero product pass it, so it is never multiplied.
143 def times(+bound: Nat, size: Nat, rest: Product) -> Product:
144 match size:
145 case 0n:
146 ZeroProduct{}
147 case 1n+(+less):
148 match rest:
149 case ZeroProduct{}:
150 ZeroProduct{}
151 case Within{+product}:
152 scaled(bound, 1n+less, product, Nat.is_lt(bound, 1n+less))
153 case Beyond{}:
154 Beyond{}
155
156 def capped(+bound: Nat, extents: List<&2, Extent>) -> Product:
157 match extents:
158 case Nil{}:
159 cap(bound, 1n)
160 case Exact{+size} <> tail:
161 times(bound, U32.to_nat(size), capped(bound, tail))
162 case Inferred{} <> tail:
163 capped(bound, tail)
164
165 def count_mismatch() -> Error:
166 TransformError{Transforms.LayoutError{Layout.ElementCountMismatch{}}}
167
168 # An Inferred size whose other sizes multiply to 0: ambiguous on an empty
169 # input, a count mismatch on any other.
170 def unfilled(count: Nat) -> Result<&2, &2, Error, List<&2, U32>>:
171 match count:
172 case 0n:
173 Fail{AmbiguousInferred{}}
174 case 1n+rest:
175 Fail{count_mismatch()}
176
177 def divided(+extents: List<&2, Extent>, split: Nat & Nat) -> Result<&2, &2, Error, List<&2, U32>>:
178 (quotient, remainder) = split
179 match remainder:
180 case 0n:
181 Done{filled(extents, U32.from_nat(quotient))}
182 case 1n+rest:
183 Fail{count_mismatch()}
184
185 # A product past the count: the inferred size is 0 on an empty input, and no
186 # size fits any other.
187 def beyond(count: Nat, +extents: List<&2, Extent>) -> Result<&2, &2, Error, List<&2, U32>>:
188 match count:
189 case 0n:
190 Done{filled(extents, 0)}
191 case 1n+rest:
192 Fail{count_mismatch()}
193
194 def inferred_one(+count: Nat, +extents: List<&2, Extent>, product: Product) -> Result<&2, &2, Error, List<&2, U32>>:
195 match product:
196 case ZeroProduct{}:
197 unfilled(count)
198 case Within{product}:
199 divided(extents, Nat.divmod(count, product))
200 case Beyond{}:
201 beyond(count, extents)
202
203 def inferred_from(+count: Nat, +extents: List<&2, Extent>, inferred: Nat) -> Result<&2, &2, Error, List<&2, U32>>:
204 match inferred:
205 case 0n:
206 Done{filled(extents, 0)}
207 case 1n+more:
208 match more:
209 case 0n:
210 inferred_one(count, extents, capped(count, extents))
211 case 1n+rest:
212 Fail{RepeatedInferred{}}
213
214 # PyTorch's infer_size: the sizes of a reshape of `count` elements. With no
215 # Inferred size, the sizes themselves; with one, it is count / product of
216 # the others when that divides exactly, the product capped at the count;
217 # more than one is refused first.
218 def inferred_dimensions(+count: Nat, +extents: List<&2, Extent>) -> Result<&2, &2, Error, List<&2, U32>>:
219 inferred_from(count, extents, inferred_count(extents))
220
221 def transformed(result: Result<&2, &1, Transforms.Error, Core.Dynamic>) -> Result<&2, &1, Error, Core.Dynamic>:
222 match result:
223 case Fail{error}:
224 Fail{TransformError{error}}
225 case Done{output}:
226 Done{output}
227
228 def reshape_inferred(input: Shape.Shape, tensor: Core.Tensor,
229 inferred: Result<&2, &2, Error, List<&2, U32>>) -> Result<&2, &1, Error, Core.Dynamic>:
230 match inferred:
231 case Fail{error}:
232 Fail{error}
233 case Done{+dimensions}:
234 transformed(Transforms.reshape(input, tensor, dimensions))
235
236 # PyTorch's reshape with -1: the sizes inferred, then the unsigned reshape.
237 def reshape(+input: Shape.Shape, tensor: Core.Tensor, +extents: List<&2, Extent>) -> Result<&2, &1, Error, Core.Dynamic>:
238 reshape_inferred(input, tensor, inferred_dimensions(Shape.elements(input), extents))
239
240 def reshape_dynamic(dynamic: Core.Dynamic, extents: List<&2, Extent>) -> Result<&2, &1, Error, Core.Dynamic>:
241 Core.Packed{+input, tensor} = dynamic
242 reshape(input, tensor, extents)
243
244 # PyTorch's permute_copy with signed axes, wrapped among the input's rank.
245 def permute_copy(+input: Shape.Shape, source: Core.Tensor, axes: List<&2, Position>) ->
246 Core.Tensor & Result<&2, &1, Transforms.Error, Core.Dynamic>:
247 Transforms.permute_copy(input, source, wrap_all(Shape.rank(input), axes))
248
249 def permute_dynamic(dynamic: Core.Dynamic, axes: List<&2, Position>) -> Core.Dynamic & Result<&2, &1, Transforms.Error, Core.Dynamic>:
250 Core.Packed{+input, source} = dynamic
251 Transforms.dynamic_result(input, permute_copy(input, source, axes))
252
253 # PyTorch's transpose_copy with signed axes, wrapped among axis_limit.
254 def transpose_copy(+input: Shape.Shape, source: Core.Tensor, a: Position, b: Position) ->
255 Core.Tensor & Result<&2, &1, Transforms.Error, Core.Dynamic>:
256 Transforms.transpose_copy(input, source, wrap(axis_limit(input), a), wrap(axis_limit(input), b))
257
258 def transpose_dynamic(dynamic: Core.Dynamic, a: Position, b: Position) -> Core.Dynamic & Result<&2, &1, Transforms.Error, Core.Dynamic>:
259 Core.Packed{+input, source} = dynamic
260 Transforms.dynamic_result(input, transpose_copy(input, source, a, b))
261
262 # A[coordinates] with signed coordinates, each wrapped among its axis's
263 # extent; and the write A[coordinates] = value.
264 def read_coordinate(+shape: Shape.Shape, tensor: Core.Tensor, coordinates: List<&2, Position>) ->
265 Core.Tensor & Result<&2, &2, Core.Error, F32>:
266 Core.read_coordinate(shape, tensor, wrap_each(Shape.dimensions(shape), coordinates))
267
268 def write_coordinate(+shape: Shape.Shape, tensor: Core.Tensor, coordinates: List<&2, Position>, value: F32) ->
269 Core.Tensor & Result<&2, &2, Core.Error, Unit>:
270 Core.write_coordinate(shape, tensor, wrap_each(Shape.dimensions(shape), coordinates), value)
271
272 # The element at a signed flat index, PyTorch's A.flatten()[index], wrapped
273 # among the element count; and the write at it.
274 def read_flat(+shape: Shape.Shape, tensor: Core.Tensor, index: Position) ->
275 Core.Tensor & Result<&2, &2, Core.Error, F32>:
276 Core.read_flat(shape, tensor, wrap(Shape.count(shape), index))
277
278 def write_flat(+shape: Shape.Shape, tensor: Core.Tensor, index: Position, +value: F32) ->
279 Core.Tensor & Result<&2, &2, Core.Error, Unit>:
280 Core.write_flat(shape, tensor, wrap(Shape.count(shape), index), value)
281
282 def read_dynamic(dynamic: Core.Dynamic, index: Position) -> Core.Dynamic & Result<&2, &2, Core.Error, F32>:
283 Core.Packed{+shape, tensor} = dynamic
284 Core.read_dynamic_result(shape, read_flat(shape, tensor, index))
285
286 def write_dynamic(dynamic: Core.Dynamic, index: Position, value: F32) -> Core.Dynamic & Result<&2, &2, Core.Error, Unit>:
287 Core.Packed{+shape, tensor} = dynamic
288 Core.write_dynamic_result(shape, write_flat(shape, tensor, index, value))
289
290 # Whether a position lies before -limit: Last{k}, the position -1 - k, with
291 # k at or past limit. No First does.
292 def before(+limit: U32, position: Position) -> Bool:
293 match position:
294 case First{k}:
295 False{}
296 case Last{k}:
297 Bool.not(U32.is_lt(k, limit))
298
299 # PyTorch's slice positions (aten::slice, get_slice_bounds) before its clamp
300 # to the axis: a negative position d becomes d + limit, the wrap, and one
301 # still below 0 becomes 0. First{k} stays k; Slice.bounds then clamps both
302 # ends to the extent.
303 def bound(+limit: U32, +position: Position) -> U32:
304 Bool.pick(U32, before(limit, position), 0, wrap(limit, position))
305
306 # The extent of axis `axis`, 0 past the rank.
307 def extent(dimensions: List<&2, U32>, +axis: U32) -> U32:
308 U32.from_nat(Slice.extent_at(dimensions, axis))
309
310 # PyTorch's input[..., start:stop:step, ...] with the slice at a signed axis
311 # `dim`, wrapped among the rank, and signed start and stop: the unsigned
312 # slice of the bounds. A rank-0 input has no axis, so every dim is refused.
313 def slice(input: Core.Dynamic, dim: Position, start: Position, stop: Position, +step: U32) ->
314 Core.Dynamic & Result<&2, &1, Slice.Error, Core.Dynamic>:
315 Core.Packed{+shape, tensor} = input
316 +axis = wrap(Shape.rank(shape), dim)
317 +n = extent(Shape.dimensions(shape), axis)
318 Slice.slice(Core.Packed{shape, tensor}, axis, bound(n, start), bound(n, stop), step)
319
320 # PyTorch's input.select(dim, index) at a signed axis and a signed index,
321 # wrapped among the extent: an index outside -extent..extent-1 stays at or
322 # past the extent, so the unsigned select refuses it with OutOfRange after
323 # its axis check.
324 def select(input: Core.Dynamic, dim: Position, index: Position) -> Core.Dynamic & Result<&2, &1, Slice.Error, Core.Dynamic>:
325 Core.Packed{+shape, tensor} = input
326 +axis = wrap(Shape.rank(shape), dim)
327 Slice.select(Core.Packed{shape, tensor}, axis, wrap(extent(Shape.dimensions(shape), axis), index))
328
329 # A narrow's start before -extent: the axis check first, as the unsigned
330 # narrow does it, then OutOfRange.
331 def narrow_before(input: Core.Dynamic, refused: Maybe<&2, Slice.Error>) -> Core.Dynamic & Result<&2, &1, Slice.Error, Core.Dynamic>:
332 match refused:
333 case Some{error}:
334 (input, Fail{error})
335 case None{}:
336 (input, Fail{Slice.OutOfRange{}})
337
338 def narrow_started(+shape: Shape.Shape, tensor: Core.Tensor, +axis: U32, +start: U32, +length: U32, early: Bool) ->
339 Core.Dynamic & Result<&2, &1, Slice.Error, Core.Dynamic>:
340 match early:
341 case False{}:
342 Slice.narrow(Core.Packed{shape, tensor}, axis, start, length)
343 case True{}:
344 narrow_before(Core.Packed{shape, tensor}, Slice.refusal(Shape.dimensions(shape), axis, 1))
345
346 # PyTorch's input.narrow(dim, start, length) at a signed axis with a signed
347 # start. PyTorch takes a start in -extent..extent: Last{k} with k below the
348 # extent is extent - 1 - k, and one before -extent is OutOfRange. A First
349 # start past the extent is refused by the unsigned narrow, which checks
350 # start + length against the extent, with the same code. A length is a
351 # U32, so PyTorch's negative lengths cannot be written.
352 def narrow(input: Core.Dynamic, dim: Position, +start: Position, +length: U32) ->
353 Core.Dynamic & Result<&2, &1, Slice.Error, Core.Dynamic>:
354 Core.Packed{+shape, tensor} = input
355 +axis = wrap(Shape.rank(shape), dim)
356 +n = extent(Shape.dimensions(shape), axis)
357 narrow_started(shape, tensor, axis, wrap(n, start), length, before(n, start))
358
359 def error_code(error: Error) -> U32:
360 match error:
361 case TransformError{error}:
362 Transforms.error_code(error)
363 case RepeatedInferred{}:
364 17
365 case AmbiguousInferred{}:
366 18