Skip to content

Commit be841e5

Browse files
Plug non-contiguous memory layout gaps (#81)
* . * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent c56426a commit be841e5

7 files changed

Lines changed: 509 additions & 128 deletions

File tree

site/notes/ndarray-design.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -362,24 +362,3 @@ M3 and M5 can proceed in parallel after M2.
362362
- `Ops[A]` typeclass with instances for `NDArray`, `Jet[NDArray]`, `Node`
363363
- Plain evaluator, forward AD evaluator, reverse AD evaluator
364364

365-
---
366-
367-
## Open questions
368-
369-
2. ~~**Default layout**~~ → Column-major (F-order) for all ranks (decided). Consistent with existing Matrix and BLAS.
370-
371-
3. **Copy semantics for views:** NDArray views share backing data. Mutation through one view is visible through others. This is the NumPy/PyTorch model and avoids unnecessary copies. Should we support copy-on-write? (Recommendation: no, too complex, just document the aliasing behavior.) Recommendation, copy on write would silently tank performance. It would be easy enough to write an immutable wrapper over the top if someone wants it. Decided. Recommendation accepted, no copy-on-write. Document aliasing semantics clearly.
372-
373-
4. **Naming:** `NDArray[A]` vs `NdArray[A]` vs `Tensor[A]`? Recommendation: `NDArray` for the data structure. Reserve `Tensor` for the AD-aware type in a future module. NDArray - decided.
374-
375-
5. **Int indexing API:** Varargs `apply(indices: Int*)` is convenient but allocates. Alternative: overloads for 1, 2, 3, 4, N cases. Or: `IArray[Int]` to signal no mutation. Recommendation: specific overloads for 1-4D, varargs for N>4. Agreed to follow recommendation.
376-
377-
6. **Broadcasting: implicit or explicit?** Implicit broadcasting (NumPy-style, where `+` silently expands shapes) vs explicit (`broadcastTo` required before binary ops). **Decided: explicit.** Rationale:
378-
- **Consistent** with existing `Array[Double]` ops which require same length — no implicit expansion anywhere in vecxt.
379-
- **Simpler binary ops**`+` is always same-shape; no broadcasting code path, no shape-mismatch error messages.
380-
- **Cleaner AD**`broadcastTo` is a first-class node in the computation graph with a clean VJP rule (sum-reduce along broadcast axes). Binary ops have trivial same-shape gradients.
381-
- **Better errors** — "cannot broadcastTo shape [4,3] from shape [2,3]" is more actionable than "cannot broadcast shapes [4,3] and [2,3] in +".
382-
- **`broadcastTo` is free** — it's a zero-copy view (stride-0). The verbosity cost is one method call that documents intent.
383-
- `NDArray.broadcastPair(a, b)` provides a convenience for the common case of broadcasting two operands to their common shape.
384-
385-
---

vecxt/src-js/doublematrix.scala

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,74 @@ object JsDoubleMatrix:
1414
inline def >=(d: Double): Matrix[Boolean] =
1515
if m.hasSimpleContiguousMemoryLayout then
1616
Matrix[Boolean](doublearrays.>=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
17-
else ???
17+
else
18+
val newArr = Array.ofDim[Boolean](m.numel)
19+
var i = 0
20+
while i < m.rows do
21+
var j = 0
22+
while j < m.cols do
23+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
24+
newArr(i + j * m.rows) = m.raw(srcIdx) >= d
25+
j += 1
26+
end while
27+
i += 1
28+
end while
29+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
1830

1931
inline def >(d: Double): Matrix[Boolean] =
2032
if m.hasSimpleContiguousMemoryLayout then
2133
Matrix[Boolean](doublearrays.>(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
22-
else ???
34+
else
35+
val newArr = Array.ofDim[Boolean](m.numel)
36+
var i = 0
37+
while i < m.rows do
38+
var j = 0
39+
while j < m.cols do
40+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
41+
newArr(i + j * m.rows) = m.raw(srcIdx) > d
42+
j += 1
43+
end while
44+
i += 1
45+
end while
46+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
2347
end if
2448
end >
2549

2650
inline def <=(d: Double): Matrix[Boolean] =
2751
if m.hasSimpleContiguousMemoryLayout then
2852
Matrix[Boolean](doublearrays.<=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
29-
else ???
53+
else
54+
val newArr = Array.ofDim[Boolean](m.numel)
55+
var i = 0
56+
while i < m.rows do
57+
var j = 0
58+
while j < m.cols do
59+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
60+
newArr(i + j * m.rows) = m.raw(srcIdx) <= d
61+
j += 1
62+
end while
63+
i += 1
64+
end while
65+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
3066
end if
3167
end <=
3268

3369
inline def <(d: Double): Matrix[Boolean] =
3470
if m.hasSimpleContiguousMemoryLayout then
3571
Matrix[Boolean](doublearrays.<(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
36-
else ???
72+
else
73+
val newArr = Array.ofDim[Boolean](m.numel)
74+
var i = 0
75+
while i < m.rows do
76+
var j = 0
77+
while j < m.cols do
78+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
79+
newArr(i + j * m.rows) = m.raw(srcIdx) < d
80+
j += 1
81+
end while
82+
i += 1
83+
end while
84+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
3785

3886
inline def *:*(bmat: Matrix[Boolean])(using inline boundsCheck: BoundsCheck): Matrix[Double] =
3987
sameDimMatCheck(m, bmat)
@@ -45,7 +93,20 @@ object JsDoubleMatrix:
4593
i += 1
4694
end while
4795
Matrix[Double](newArr, (m.rows, m.cols))
48-
else ???
96+
else
97+
val newArr = Array.ofDim[Double](m.numel)
98+
var i = 0
99+
while i < m.rows do
100+
var j = 0
101+
while j < m.cols do
102+
val mIdx = m.offset + i * m.rowStride + j * m.colStride
103+
val bIdx = bmat.offset + i * bmat.rowStride + j * bmat.colStride
104+
newArr(i + j * m.rows) = if bmat.raw(bIdx) then m.raw(mIdx) else 0.0
105+
j += 1
106+
end while
107+
i += 1
108+
end while
109+
Matrix[Double](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
49110
end if
50111
end *:*
51112

vecxt/src-jvm/doublematrix.scala

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,20 @@ object JvmDoubleMatrix:
8585
i += 1
8686
end while
8787
Matrix(newArr, m.rows, m.cols)
88-
else ???
88+
else
89+
val newArr = Array.ofDim[Double](m.numel)
90+
var i = 0
91+
while i < m.rows do
92+
var j = 0
93+
while j < m.cols do
94+
val mIdx = m.offset + i * m.rowStride + j * m.colStride
95+
val bIdx = bmat.offset + i * bmat.rowStride + j * bmat.colStride
96+
newArr(i + j * m.rows) = if bmat.raw(bIdx) then m.raw(mIdx) else 0.0
97+
j += 1
98+
end while
99+
i += 1
100+
end while
101+
Matrix[Double](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
89102
end if
90103
end *:*
91104

@@ -137,22 +150,70 @@ object JvmDoubleMatrix:
137150
inline def >=(d: Double): Matrix[Boolean] =
138151
if m.hasSimpleContiguousMemoryLayout then
139152
Matrix[Boolean](vecxt.doublearrays.>=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
140-
else ???
153+
else
154+
val newArr = Array.ofDim[Boolean](m.numel)
155+
var i = 0
156+
while i < m.rows do
157+
var j = 0
158+
while j < m.cols do
159+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
160+
newArr(i + j * m.rows) = m.raw(srcIdx) >= d
161+
j += 1
162+
end while
163+
i += 1
164+
end while
165+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
141166

142167
inline def >(d: Double): Matrix[Boolean] =
143168
if m.hasSimpleContiguousMemoryLayout then
144169
Matrix[Boolean](vecxt.doublearrays.>(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
145-
else ???
170+
else
171+
val newArr = Array.ofDim[Boolean](m.numel)
172+
var i = 0
173+
while i < m.rows do
174+
var j = 0
175+
while j < m.cols do
176+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
177+
newArr(i + j * m.rows) = m.raw(srcIdx) > d
178+
j += 1
179+
end while
180+
i += 1
181+
end while
182+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
146183

147184
inline def <=(d: Double): Matrix[Boolean] =
148185
if m.hasSimpleContiguousMemoryLayout then
149186
Matrix[Boolean](vecxt.doublearrays.<=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
150-
else ???
187+
else
188+
val newArr = Array.ofDim[Boolean](m.numel)
189+
var i = 0
190+
while i < m.rows do
191+
var j = 0
192+
while j < m.cols do
193+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
194+
newArr(i + j * m.rows) = m.raw(srcIdx) <= d
195+
j += 1
196+
end while
197+
i += 1
198+
end while
199+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
151200

152201
inline def <(d: Double): Matrix[Boolean] =
153202
if m.hasSimpleContiguousMemoryLayout then
154203
Matrix[Boolean](vecxt.doublearrays.<(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
155-
else ???
204+
else
205+
val newArr = Array.ofDim[Boolean](m.numel)
206+
var i = 0
207+
while i < m.rows do
208+
var j = 0
209+
while j < m.cols do
210+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
211+
newArr(i + j * m.rows) = m.raw(srcIdx) < d
212+
j += 1
213+
end while
214+
i += 1
215+
end while
216+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
156217

157218
/** Adds the elements of this vector to the matrix with broadcasting behavior.
158219
*

vecxt/src-native/doublematrix_native.scala

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,20 @@ object NativeDoubleMatrix:
2020
i += 1
2121
end while
2222
Matrix[Double](newArr, (m.rows, m.cols))
23-
else ???
23+
else
24+
val newArr = Array.ofDim[Double](m.numel)
25+
var i = 0
26+
while i < m.rows do
27+
var j = 0
28+
while j < m.cols do
29+
val mIdx = m.offset + i * m.rowStride + j * m.colStride
30+
val bIdx = bmat.offset + i * bmat.rowStride + j * bmat.colStride
31+
newArr(i + j * m.rows) = if bmat.raw(bIdx) then m.raw(mIdx) else 0.0
32+
j += 1
33+
end while
34+
i += 1
35+
end while
36+
Matrix[Double](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
2437
end if
2538
end *:*
2639

@@ -76,26 +89,74 @@ object NativeDoubleMatrix:
7689
inline def >=(d: Double): Matrix[Boolean] =
7790
if m.hasSimpleContiguousMemoryLayout then
7891
Matrix[Boolean](doublearrays.>=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
79-
else ???
92+
else
93+
val newArr = Array.ofDim[Boolean](m.numel)
94+
var i = 0
95+
while i < m.rows do
96+
var j = 0
97+
while j < m.cols do
98+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
99+
newArr(i + j * m.rows) = m.raw(srcIdx) >= d
100+
j += 1
101+
end while
102+
i += 1
103+
end while
104+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
80105

81106
inline def >(d: Double): Matrix[Boolean] =
82107
if m.hasSimpleContiguousMemoryLayout then
83108
Matrix[Boolean](doublearrays.>(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
84-
else ???
109+
else
110+
val newArr = Array.ofDim[Boolean](m.numel)
111+
var i = 0
112+
while i < m.rows do
113+
var j = 0
114+
while j < m.cols do
115+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
116+
newArr(i + j * m.rows) = m.raw(srcIdx) > d
117+
j += 1
118+
end while
119+
i += 1
120+
end while
121+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
85122
end if
86123
end >
87124

88125
inline def <=(d: Double): Matrix[Boolean] =
89126
if m.hasSimpleContiguousMemoryLayout then
90127
Matrix[Boolean](doublearrays.<=(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
91-
else ???
128+
else
129+
val newArr = Array.ofDim[Boolean](m.numel)
130+
var i = 0
131+
while i < m.rows do
132+
var j = 0
133+
while j < m.cols do
134+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
135+
newArr(i + j * m.rows) = m.raw(srcIdx) <= d
136+
j += 1
137+
end while
138+
i += 1
139+
end while
140+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
92141
end if
93142
end <=
94143

95144
inline def <(d: Double): Matrix[Boolean] =
96145
if m.hasSimpleContiguousMemoryLayout then
97146
Matrix[Boolean](doublearrays.<(m.raw)(d), m.shape)(using BoundsCheck.DoBoundsCheck.no)
98-
else ???
147+
else
148+
val newArr = Array.ofDim[Boolean](m.numel)
149+
var i = 0
150+
while i < m.rows do
151+
var j = 0
152+
while j < m.cols do
153+
val srcIdx = m.offset + i * m.rowStride + j * m.colStride
154+
newArr(i + j * m.rows) = m.raw(srcIdx) < d
155+
j += 1
156+
end while
157+
i += 1
158+
end while
159+
Matrix[Boolean](newArr, m.rows, m.cols)(using BoundsCheck.DoBoundsCheck.no)
99160

100161
inline def `matmulInPlace!`(
101162
b: Matrix[Double],

0 commit comments

Comments
 (0)