cumo 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +113 -8
- data/bench/cumo_probe.rb +1 -1
- data/cumo.gemspec +6 -0
- data/ext/cumo/cuda/memory_pool_impl.cpp +32 -22
- data/ext/cumo/cuda/memory_pool_impl.hpp +3 -1
- data/ext/cumo/cuda/memory_pool_impl_test.cpp +19 -25
- data/ext/cumo/include/cumo/check.h +11 -3
- data/ext/cumo/include/cumo/cuda/cudnn.h +3 -6
- data/ext/cumo/include/cumo/indexer.h +16 -0
- data/ext/cumo/include/cumo/intern.h +2 -0
- data/ext/cumo/include/cumo/reduce_kernel.h +253 -28
- data/ext/cumo/include/cumo/row_kernel.h +49 -17
- data/ext/cumo/include/cumo/row_method.h +101 -0
- data/ext/cumo/include/cumo/types/bf16_macro.h +6 -178
- data/ext/cumo/include/cumo/types/bf16_macro_kernel.h +6 -200
- data/ext/cumo/include/cumo/types/f16_macro.h +191 -0
- data/ext/cumo/include/cumo/types/f16_macro_kernel.h +214 -0
- data/ext/cumo/include/cumo/types/float_macro.h +7 -0
- data/ext/cumo/include/cumo/types/float_macro_kernel.h +7 -0
- data/ext/cumo/include/cumo/types/half_macro.h +4 -176
- data/ext/cumo/include/cumo/types/half_macro_kernel.h +4 -198
- data/ext/cumo/include/cumo.h +2 -2
- data/ext/cumo/narray/data.c +15 -6
- data/ext/cumo/narray/data_kernel.cu +110 -0
- data/ext/cumo/narray/gen/def/bfloat.rb +2 -1
- data/ext/cumo/narray/gen/def/bit.rb +1 -0
- data/ext/cumo/narray/gen/def/dcomplex.rb +1 -0
- data/ext/cumo/narray/gen/def/dfloat.rb +1 -0
- data/ext/cumo/narray/gen/def/hfloat.rb +2 -1
- data/ext/cumo/narray/gen/def/int16.rb +1 -0
- data/ext/cumo/narray/gen/def/int32.rb +1 -0
- data/ext/cumo/narray/gen/def/int64.rb +1 -0
- data/ext/cumo/narray/gen/def/int8.rb +1 -0
- data/ext/cumo/narray/gen/def/robject.rb +1 -0
- data/ext/cumo/narray/gen/def/scomplex.rb +1 -0
- data/ext/cumo/narray/gen/def/sfloat.rb +1 -0
- data/ext/cumo/narray/gen/def/uint16.rb +1 -0
- data/ext/cumo/narray/gen/def/uint32.rb +1 -0
- data/ext/cumo/narray/gen/def/uint64.rb +1 -0
- data/ext/cumo/narray/gen/def/uint8.rb +1 -0
- data/ext/cumo/narray/gen/narray_def.rb +35 -1
- data/ext/cumo/narray/gen/spec.rb +3 -0
- data/ext/cumo/narray/gen/tmpl/accum_binary.c +63 -5
- data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +21 -5
- data/ext/cumo/narray/gen/tmpl/batch_norm.c +1 -1
- data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +2 -2
- data/ext/cumo/narray/gen/tmpl/binary.c +3 -9
- data/ext/cumo/narray/gen/tmpl/conv.c +2 -2
- data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +2 -2
- data/ext/cumo/narray/gen/tmpl/conv_transpose.c +2 -2
- data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +1 -1
- data/ext/cumo/narray/gen/tmpl/gemm.c +0 -6
- data/ext/cumo/narray/gen/tmpl/layer_norm.c +8 -61
- data/ext/cumo/narray/gen/tmpl/pooling_backward.c +1 -1
- data/ext/cumo/narray/gen/tmpl/pooling_forward.c +1 -1
- data/ext/cumo/narray/gen/tmpl/quantize_symmetric.c +74 -0
- data/ext/cumo/narray/gen/tmpl/quantize_symmetric_kernel.cu +67 -0
- data/ext/cumo/narray/gen/tmpl/rms_norm.c +38 -0
- data/ext/cumo/narray/gen/tmpl/rms_norm_kernel.cu +57 -0
- data/ext/cumo/narray/gen/tmpl/softmax.c +4 -39
- data/ext/cumo/narray/gen/tmpl/softmax_kernel.cu +2 -2
- data/ext/cumo/narray/gen/tmpl/store_from.c +1 -16
- data/ext/cumo/narray/index.c +33 -24
- data/ext/cumo/narray/index_kernel.cu +27 -0
- data/ext/cumo/narray/math.c +38 -8
- data/ext/cumo/narray/narray.c +58 -14
- data/ext/cumo/narray/ndloop.c +137 -1
- data/test/bit_test.rb +52 -14
- data/test/fused_test.rb +300 -24
- data/test/math_test.rb +105 -0
- data/test/narray_test.rb +274 -12
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f27ebdaa195fa967c144d6178ad3f9fbeca551162f8edacac205d5edd5d0fb6
|
|
4
|
+
data.tar.gz: 3d87faa55ab7c4c0e52bee3afdb0116d9d463fa778c408a46e200bff0cabdf81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b498a760e59725c15aa3e651ecbada01e52517833fd3132a860dbfe459e354f4afba47207cc33f2c94963f58f19a8a1d7e52ce722e51065876d637c8ddfc94b
|
|
7
|
+
data.tar.gz: bbda42b2ef7ce7ea882982a015bec06ff058ad42a2b72e4c8953e75facfcb64657c8da4b69d63666cf4c06d3c265dc7e0acf54c431450f181b39cbeebed9560e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
# 0.9.0 (2026/09/19)
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
* An integer array divided by a zero Ruby number raises `ZeroDivisionError` before the kernel runs rather than after it, so `a.inplace / 0` leaves `a` alone where it wrote over it first. The divisor was on the host all along, so the flag the device raised and the `cudaDeviceSynchronize` that read it back are gone: `Cumo::Int32.new(1, 768)` costs 2.34us for `a / 2` where it cost 9.27, and 2.20 for `a % 2` where it cost 7.88. `2 / a` and an array by an array keep the flag, the divisor really being on the device there (PR #466)
|
|
6
|
+
|
|
7
|
+
Fixes:
|
|
8
|
+
|
|
9
|
+
* Fix `mulsum` reducing everything where the receiver is a view that marked its axes with `:sum`, `:reduce` or `:+` and the operands have different dtypes. The marking lives on the array the method is asked of, and `mulsum` answers another class by putting a different array in the receiver's place, so `Cumo::Int8.new(4, 6)[:sum, true].mulsum(a_float)` answered a scalar where the same call with matching dtypes answers a row of six. numo answers the same wrong thing (PR #480)
|
|
10
|
+
* Fix an elementwise operation whose inplace receiver shares memory with an operand that walks it differently, `a.inplace + a.transpose` answering something new on every run: the element a thread reads is one another thread is writing, and nothing orders the two on a device. An operand sitting exactly where the output does is left alone, which is the common `a.inplace + a` (PR #473)
|
|
11
|
+
* Fix `to_binary` handing out whatever lay past the end of a `Cumo::Bit` array whose size is not a multiple of eight. `Cumo::Bit.new(24).fill(1)[0..3].to_binary` answered `[255]` where four bits belong to it, and a view copied first answered the memory pool's contents, so the same expression could answer differently from one run to the next. `marshal_dump` goes through here too (PR #471)
|
|
12
|
+
* Fix an inplace view built from an index array reducing from the wrong rows. The copy that makes such a view contiguous carried `CUMO_NDF_INPLACE`, so for an inplace receiver it answered self and the reduction walked the index-backed view it meant to have copied. `median` and `max_index` answered their own wrong rows the same way, and inplace survives an operation, so `(v.inplace * 2).sum(axis: 2)` reached it too (PR #461)
|
|
13
|
+
* Hold `layer_norm`, `rms_norm` and `softmax` to the same checks. `softmax` measured the result by its element count where the other two compare the shape, so an `allocate` that reshaped either side to the same count left every check true: reshaping the result answered the right values under the wrong shape, and reshaping the receiver normalized along the wrong axis (PR #459)
|
|
14
|
+
|
|
15
|
+
Changes:
|
|
16
|
+
|
|
17
|
+
* Add `rms_norm`, which normalizes a row in one kernel rather than the six it is written out of. This is `layer_norm` without the centring, the layer Llama and the models after it normalize with, so it takes no beta. On SFloat `[1, 768]` it goes from 14.2 to 2.9us and `[4096, 768]` from 122.5 to 38.5. No shape is slower (PR #458)
|
|
18
|
+
* Add `silu` to `Cumo::NMath`, which takes one kernel rather than the five it is written out of: 768 elements go from 13.4 to 3.6us. It follows `torch.nn.functional.silu`, answering within one unit in the last place of the true value at the nine double points measured, and matching it at the edges (PR #460)
|
|
19
|
+
* Add `quantize_symmetric`, which quantizes a row to `Cumo::Int8` in one kernel rather than the six it is written out of and hands the scale back. A launch costs about two microseconds whatever it is handed, so a short row pays for those rather than for its bytes (PR #476)
|
|
20
|
+
* Speed up `mulsum`. Operands of different dtypes are read where they already are and converted one element at a time rather than cast into an array of the wider type first, so `SFloat#mulsum(Int8)` at `[4096, 4096]` goes from 384 to 53us. A broadcast operand no longer falls off the fast path, `1000x12x64` against `1000x12x1` over axis 0 going from 13.5 to 4.9us, and the thread layout takes the shorter step of the two operands rather than the first one's, so `DFloat[64,1].mulsum(DFloat[64,1024], axis: 1)` goes from 16.26 to 5.68 whichever order it is written in (PR #479, PR #477, PR #475)
|
|
21
|
+
* Fix every allocation slowing down as the memory pool fragments, the arena having been rebuilt whenever an allocation emptied a bin. GPT-2 124M generating 256 tokens goes from 11.9 to 3.2 seconds in fp16, where the pool ends up with 4035 free chunks against fp32's 71 (PR #474)
|
|
22
|
+
* Range check an NArray index on the device rather than copying the indices to the host and back. `Cumo::SFloat.new(50257, 768)`, `a[idx, true]`: 4096 indices go from 58.2 to 22.2us and 64 from 20.8 to 13.8. Nothing reads managed memory from the host on that path any more, which is what breaks under Ractor (sonots/cumo#180) (PR #470)
|
|
23
|
+
* Copy a transposed view through the shared-memory tile the typed store has used since 0.7.0. `Cumo::SFloat.new(4096, 1024)`: `a.transpose.copy` goes from 338 to 97us, level with `a.transpose.dup` at 102, and `a.transpose.max_index(axis: 1)` from 362 to 107. Every reduction that needs a contiguous operand goes through this copy (PR #468)
|
|
24
|
+
* Reduce a view built from an index array without staging it in a buffer first. Copying a `[6250, 64]` view of a `[100000, 64]` array goes from 13.9 to 7.2us and reducing it from 25.1 to 18.3 (PR #462)
|
|
25
|
+
* Let `store_binary` reach a view it could only read back from. `to_binary` reads any view, so `v.store_binary(v.to_binary)` raised for the same `v` that had just produced the string. A view the bytes cannot move into directly now takes them through an array of its own class and shape (PR #472)
|
|
26
|
+
* Answer `respond_to?` for the methods `Cumo::NMath` dispatches. Every one of them arrives through `method_missing`, so `respond_to?` answered false for `sqrt` and `exp` alike, which code that checks for a feature before calling it reads as absence (PR #463)
|
|
27
|
+
* Take one Ruby method dispatch out of every ndloop call, the argument list always being a plain Array where `rb_obj_dup` took the general path: about 800 fewer retired instructions per operation, 3.4% of the 23,200 an operation takes (PR #465)
|
|
28
|
+
* Tell which array a type check rejected and what it wanted. It said "invalid NArray type (class)", naming neither, so two checks side by side read exactly alike; it now says "gamma must be Cumo::SFloat, not Cumo::HFloat" (PR #478)
|
|
29
|
+
* Say in the README that `reshape` answers a copy where slicing answers a view, priced across four shapes: on a GPU it is an allocation and a copy kernel every call, 58.1us and 12MB for a `4096x768` SFloat against 0.22us and nothing for `reshape!` (PR #457)
|
|
30
|
+
* Show the changelog, source and issue links on the gem page, and require multi-factor authentication to push the gem (PR #456)
|
|
31
|
+
* Share one body between the half and bfloat16 macros so a fix reaches both, 399 duplicated lines where it would have reached one. The compiled code is unchanged: the device SASS is identical (PR #464)
|
|
32
|
+
* Keep the index-array test in one place so copy and store cannot drift apart, 172 generated copies of it becoming one (PR #467)
|
|
33
|
+
* Drop the `Cumo::NMath::DISPATCH` rows no argument can reach. A bare Ruby numeric takes an NArray row, so the `Integer`, `Float` and `Complex` ones were never read: 944 calls over 16 kinds of argument reached them zero times (PR #469)
|
|
34
|
+
|
|
1
35
|
# 0.8.0 (2026/09/16)
|
|
2
36
|
|
|
3
37
|
Breaking changes:
|
data/README.md
CHANGED
|
@@ -178,6 +178,44 @@ x += p_dir * alpha # and consumed there, without crossing the bus
|
|
|
178
178
|
|
|
179
179
|
Read the value back once the loop is done, or every k iterations if it has to test something.
|
|
180
180
|
|
|
181
|
+
### Reshape Copies, Reshape! Does Not
|
|
182
|
+
|
|
183
|
+
`reshape` answers a copy of the whole array, never a view.
|
|
184
|
+
Slicing answers a view, so the two look alike and are not:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
a = Cumo::SFloat.new(4, 6).seq
|
|
188
|
+
a[0..1, true][0, 0] = 77.0 # a view, so a changes
|
|
189
|
+
a.reshape(2, 12)[0, 0] = 99.0 # a copy, so a does not
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This is what Numo does too, and numpy is where the expectation comes from: there `reshape` answers a view whenever the strides allow one.
|
|
193
|
+
On a GPU the difference is an allocation and a copy kernel, paid every call.
|
|
194
|
+
`reshape!` changes the receiver in place and costs neither.
|
|
195
|
+
RTX 5070 Ti Laptop, `Cumo::SFloat`, 200 calls a measurement:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
shape reshape reshape! the copy allocates
|
|
199
|
+
1x768 5.1 us 0.04 us 3 KB
|
|
200
|
+
1024x768 39.1 us 0.18 us 3 MB
|
|
201
|
+
4096x768 58.1 us 0.22 us 12 MB
|
|
202
|
+
8192x768 142.7 us 0.43 us 24 MB
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`reshape!` is host-side bookkeeping, so it stays under a microsecond whatever the array weighs.
|
|
206
|
+
The `reshape` column is the copy, and it grows with the bytes.
|
|
207
|
+
|
|
208
|
+
The catch is that `reshape!` changes the array everything else is holding.
|
|
209
|
+
It fits a temporary the calling expression owns, and not an argument, an ivar, or anything a cache still points at:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
x = a * b # a temporary nothing else holds
|
|
213
|
+
x.reshape!(t, n, h) # free
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Where the array is not yours to change, the copy is the price of the shape.
|
|
217
|
+
Reach for `reshape!` when a profile says the copies are worth removing, not by default.
|
|
218
|
+
|
|
181
219
|
### Ruby Floats In NMath Promote To Double
|
|
182
220
|
|
|
183
221
|
`Cumo::NMath` picks the module it dispatches to from every argument it is given, and a Ruby `Float` counts as a `DFloat` there.
|
|
@@ -224,14 +262,18 @@ The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float
|
|
|
224
262
|
|
|
225
263
|
### Fused Operations
|
|
226
264
|
|
|
227
|
-
`layer_norm` and `softmax` normalize along the last axis in one kernel each.
|
|
228
|
-
Written out of the operators they take nine launches and
|
|
265
|
+
`layer_norm`, `rms_norm` and `softmax` normalize along the last axis in one kernel each, and `quantize_symmetric` takes it to 8-bit integers in one more.
|
|
266
|
+
Written out of the operators they take nine launches, six, five and six, and a launch costs about two microseconds whatever it is handed, so a short row pays for the launches rather than for its bytes.
|
|
229
267
|
|
|
230
268
|
```ruby
|
|
231
269
|
y = x.layer_norm(gamma, beta, eps: 1e-5) # (x - mean) / sqrt(var + eps) * gamma + beta
|
|
270
|
+
z = x.rms_norm(gamma, eps: 1e-5) # x / sqrt(mean(x * x) + eps) * gamma
|
|
232
271
|
probs = scores.softmax # exp(x - max) / sum(exp(x - max))
|
|
272
|
+
xq, scale = x.quantize_symmetric # scale = max(|x|) / 127, xq = round(x / scale)
|
|
233
273
|
```
|
|
234
274
|
|
|
275
|
+
`quantize_symmetric` answers a `Cumo::Int8` shaped like self and the scale of every row, which is self's shape without its last axis. `xq * scale[false, :new]` is what the row stood for. A row of zeros has no scale to divide by and answers zero for both, and a row holding an infinity or a NaN answers that in its scale and zeros in the row, since neither is a value 8 bits could carry. The scale comes back in the class the reduction accumulates in, which is `Cumo::SFloat` for `Cumo::HFloat` and `Cumo::BFloat` and self's own otherwise.
|
|
276
|
+
|
|
235
277
|
On an RTX 5070 Ti Laptop, against the same arithmetic spelled with operators, in microseconds:
|
|
236
278
|
|
|
237
279
|
```
|
|
@@ -248,23 +290,58 @@ shape fused written fused written fused written
|
|
|
248
290
|
256 x 768 8.8 13.9 23.4 35.1 6.8 18.5
|
|
249
291
|
4096 x 768 49.2 122.7 318.3 549.0 28.2 109.8
|
|
250
292
|
1 x 1000000 27.8 44.0 108.6 148.5 32.1 53.8
|
|
293
|
+
|
|
294
|
+
rms_norm SFloat DFloat HFloat
|
|
295
|
+
shape fused written fused written fused written
|
|
296
|
+
1 x 768 2.9 14.2 5.8 14.1 3.1 15.1
|
|
297
|
+
256 x 768 4.6 16.7 6.9 20.2 5.2 15.5
|
|
298
|
+
4096 x 768 38.5 122.5 130.8 367.0 19.2 80.8
|
|
299
|
+
1 x 1000000 12.5 35.8 23.7 102.2 12.0 33.2
|
|
251
300
|
```
|
|
252
301
|
|
|
253
|
-
|
|
302
|
+
```
|
|
303
|
+
quantize_symmetric SFloat DFloat HFloat
|
|
304
|
+
shape fused written fused written fused written
|
|
305
|
+
1 x 768 3.7 20.2 6.0 15.7 2.3 11.9
|
|
306
|
+
256 x 768 4.7 15.0 20.2 21.3 4.7 13.0
|
|
307
|
+
4096 x 768 24.4 105.4 289.5 333.3 23.8 80.3
|
|
308
|
+
1 x 1000000 18.8 40.8 97.1 81.6 15.7 37.5
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The tables were taken in separate sessions, so read each row against the row beside it and not across the tables.
|
|
312
|
+
|
|
313
|
+
`quantize_symmetric` in `Cumo::DFloat` is worth less than the others and loses outright on a million elements, whatever shape they are in. The division it does per element is what costs: on this card one row of a million takes 44.0 us to divide in double against 10.8 in single, where the reduction over the same row takes 25.3 and 15.4. Six kernels give that division a kernel of its own to fill the device with, and one kernel leaves it behind the reduction.
|
|
314
|
+
|
|
315
|
+
All three pay off in every precision, by the most where the row is short enough that the launches were all it was doing, and by the least in double, where the reduction itself costs more than the launches ever did.
|
|
254
316
|
The memory clock on this card steps between 9001 and 11001 MHz under a benchmark this short, and the absolute figures move with it.
|
|
255
317
|
The ratios hold across the steps.
|
|
256
318
|
|
|
257
|
-
|
|
319
|
+
All three normalize along the last axis only.
|
|
258
320
|
Other axes are reachable through `transpose`.
|
|
259
|
-
`layer_norm` divides the variance by the row length and not by one less, which is how the layer is defined and unlike `var
|
|
260
|
-
|
|
321
|
+
`layer_norm` divides the variance by the row length and not by one less, which is how the layer is defined and unlike `var`; `rms_norm` divides its mean square the same way.
|
|
322
|
+
All three answer a new array and ignore `inplace!`, and a non-contiguous receiver is copied once rather than walked.
|
|
261
323
|
`gamma` and `beta` must be one-dimensional, as long as the last axis, and of the same class as the receiver.
|
|
262
324
|
|
|
325
|
+
`rms_norm` is `layer_norm` without the centring, and takes no `beta`, which is the layer Llama and the models after it normalize with.
|
|
326
|
+
The two agree on a row whose mean is already zero and differ on every other row, so they are not interchangeable:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
x = Cumo::SFloat[[1.0, 2.0, 3.0, 4.0]]
|
|
330
|
+
ones = Cumo::SFloat.ones(4)
|
|
331
|
+
zeros = Cumo::SFloat.zeros(4)
|
|
332
|
+
x.rms_norm(ones) #=> [[0.365, 0.730, 1.095, 1.461]]
|
|
333
|
+
x.layer_norm(ones, zeros) #=> [[-1.342, -0.447, 0.447, 1.342]]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
All three accumulate in single precision, so a half row that squares past the 65504 half holds is still answered the way double answers it.
|
|
337
|
+
bfloat16 carries a float's exponent rather than a half's, so a bfloat16 row that squares past what a float holds overflows the accumulator too: `Cumo::BFloat[[-3e20, 3e20, 3e20, -3e20]].rms_norm(ones)` answers zeros where double answers ones.
|
|
338
|
+
|
|
263
339
|
`softmax` subtracts the row maximum before exponentiating, so a row masked with `-Float::INFINITY` answers zeros rather than `NaN`.
|
|
264
340
|
A row that is entirely `-Float::INFINITY` answers `NaN`, as the written-out form does.
|
|
265
341
|
|
|
266
|
-
|
|
342
|
+
`layer_norm` and `softmax` above, with `gelu_tanh` from the section below, are where a GPT-2 style transformer spends most of its launches.
|
|
267
343
|
Decoding one token of GPT-2 124M written against Numo's API takes 640 of them, and calling these three instead removes 332: 200 for the layer norms, 84 for the activations and 48 for the softmaxes.
|
|
344
|
+
A Llama style one spends them on `rms_norm` and `silu` instead, which is why they are here.
|
|
268
345
|
|
|
269
346
|
### Two Spellings Of gelu
|
|
270
347
|
|
|
@@ -284,6 +361,34 @@ Both follow PyTorch at the edges rather than the limit: a large negative `x`
|
|
|
284
361
|
runs out of significant digits and answers zero, and `-Float::INFINITY` answers
|
|
285
362
|
`NaN`.
|
|
286
363
|
|
|
364
|
+
### SiLU
|
|
365
|
+
|
|
366
|
+
`Cumo::NMath.silu` is `x * sigmoid(x)`, the activation Llama and the models after it use where GPT-2 uses `gelu_tanh`.
|
|
367
|
+
It is also called Swish.
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
Cumo::NMath.silu(x) # x / (1 + exp(-x)), in one kernel rather than five
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Written out of the operators it costs five launches, and one kernel runs 1.1x to 4.3x faster on an RTX 5070 Ti Laptop, in microseconds:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
silu SFloat DFloat HFloat
|
|
377
|
+
elements fused written fused written fused written
|
|
378
|
+
768 3.6 13.4 3.6 9.5 3.4 14.3
|
|
379
|
+
786432 7.0 25.9 90.5 102.4 5.2 19.2
|
|
380
|
+
16777216 353.2 1312.9 1984.7 3258.8 199.6 851.8
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The three rows are three regimes rather than one curve: 768 elements pay for the launches, 786432 fit in L2 and move faster than this card reads from memory, and 16777216 are what it costs from DRAM.
|
|
384
|
+
|
|
385
|
+
It follows `torch.nn.functional.silu`, answering within one unit in the last place of the true value at the nine double points measured, as torch does.
|
|
386
|
+
At the edges it matches torch exactly: `-Float::INFINITY` answers `NaN`, since that is what infinity times zero is, and a large negative `x` answers a signed zero.
|
|
387
|
+
|
|
388
|
+
Where that zero starts depends on the type, and not on `exp` alone.
|
|
389
|
+
Single and bfloat16 reach it at -89, where `exp(-x)` passes what a float holds, and double never does.
|
|
390
|
+
Half reaches it at -21, because the round back to half gets there first.
|
|
391
|
+
|
|
287
392
|
### Half Precision
|
|
288
393
|
|
|
289
394
|
`Cumo::HFloat`, also reachable as `Cumo::Float16`, holds IEEE binary16: one sign bit, five of exponent and ten of mantissa.
|
|
@@ -430,7 +535,7 @@ The two do not contain each other, so an expression mixing them promotes to `Cum
|
|
|
430
535
|
|
|
431
536
|
Everything else promotes as `Cumo::SFloat` does, so an integer array or a Ruby Float mixed in stays bfloat16 while anything wider takes over.
|
|
432
537
|
|
|
433
|
-
`layer_norm`, `softmax` and both spellings of `gelu` take it, and so do the reductions, `sort`, `median`, `cumsum`, `rand` and `dot`.
|
|
538
|
+
`layer_norm`, `rms_norm`, `softmax`, `silu` and both spellings of `gelu` take it, and so do the reductions, `sort`, `median`, `cumsum`, `rand` and `dot`.
|
|
434
539
|
The cuDNN methods take it too: `conv`, `conv_transpose`, `conv_grad_w`, `max_pool`, `avg_pool` and the three batch norm entries.
|
|
435
540
|
|
|
436
541
|
Reductions accumulate in single precision and round once at the end, exactly as they do for half, so a sum passes 256 without stopping there:
|
data/bench/cumo_probe.rb
CHANGED
|
@@ -312,7 +312,7 @@ op(:scalar, 'scalar_left') { |c| c.scalar - c.sc.inplace }
|
|
|
312
312
|
|
|
313
313
|
# NMath
|
|
314
314
|
%w[sqrt exp exp2 exp10 expm1 log log2 log10 log1p sin cos tan asin acos atan
|
|
315
|
-
sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc gelu gelu_tanh].each do |f|
|
|
315
|
+
sinh cosh tanh asinh acosh atanh cbrt erf erfc sinc gelu gelu_tanh silu].each do |f|
|
|
316
316
|
op(:math, f, :float) { |c| XM::NMath.send(f, c.usc.inplace) }
|
|
317
317
|
end
|
|
318
318
|
op(:math, 'atan2', :float) { |c| XM::NMath.atan2(c.sc.inplace, c.b) }
|
data/cumo.gemspec
CHANGED
|
@@ -16,6 +16,12 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.homepage = "https://github.com/sonots/cumo"
|
|
17
17
|
spec.license = "MIT"
|
|
18
18
|
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/sonots/cumo/blob/master/CHANGELOG.md"
|
|
22
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/sonots/cumo/issues"
|
|
23
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
24
|
+
|
|
19
25
|
spec.required_ruby_version = ">= 3.0.0"
|
|
20
26
|
|
|
21
27
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
@@ -108,7 +108,16 @@ bool SingleDeviceMemoryPool::RemoveFromFreeList(size_t size, std::shared_ptr<Chu
|
|
|
108
108
|
}
|
|
109
109
|
assert(arena.size() > arena_index);
|
|
110
110
|
FreeList& free_list = arena[arena_index];
|
|
111
|
-
|
|
111
|
+
if (!EraseFromFreeList(free_list, chunk)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// A bin left empty here would be walked by every later search, and the
|
|
115
|
+
// merge path below Free empties one whenever it takes a neighbour out.
|
|
116
|
+
if (free_list.empty()) {
|
|
117
|
+
arena.erase(arena.begin() + arena_index);
|
|
118
|
+
arena_index_map.erase(arena_index_map.begin() + arena_index);
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
intptr_t SingleDeviceMemoryPool::Malloc(size_t size, cudaStream_t stream_ptr) {
|
|
@@ -132,6 +141,7 @@ intptr_t SingleDeviceMemoryPool::Malloc(size_t size, cudaStream_t stream_ptr) {
|
|
|
132
141
|
|
|
133
142
|
// find best-fit, or a smallest larger allocation
|
|
134
143
|
Arena& arena = GetArena(stream_ptr);
|
|
144
|
+
ArenaIndexMap& arena_index_map = GetArenaIndexMap(stream_ptr);
|
|
135
145
|
size_t arena_index = GetArenaIndex(size, stream_ptr);
|
|
136
146
|
size_t arena_length = arena.size();
|
|
137
147
|
for (size_t i = arena_index; i < arena_length; ++i) {
|
|
@@ -141,10 +151,15 @@ intptr_t SingleDeviceMemoryPool::Malloc(size_t size, cudaStream_t stream_ptr) {
|
|
|
141
151
|
}
|
|
142
152
|
chunk = PopFromFreeList(free_list);
|
|
143
153
|
if (free_list.empty()) {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
154
|
+
// Drop just this bin. Rebuilding the whole arena instead
|
|
155
|
+
// copied every other bin's free list, and a free list holds
|
|
156
|
+
// shared pointers, so a fragmented pool touched the reference
|
|
157
|
+
// count of every chunk it held on every allocation.
|
|
158
|
+
// This leaves free_list, the elements of arena_index_map and
|
|
159
|
+
// every iterator from i on dangling, so nothing below may
|
|
160
|
+
// touch them.
|
|
161
|
+
arena.erase(arena.begin() + i);
|
|
162
|
+
arena_index_map.erase(arena_index_map.begin() + i);
|
|
148
163
|
}
|
|
149
164
|
break;
|
|
150
165
|
}
|
|
@@ -252,7 +267,7 @@ bool SingleDeviceMemoryPool::Free(intptr_t ptr) {
|
|
|
252
267
|
return true;
|
|
253
268
|
}
|
|
254
269
|
|
|
255
|
-
void SingleDeviceMemoryPool::CompactIndex(cudaStream_t stream_ptr
|
|
270
|
+
void SingleDeviceMemoryPool::CompactIndex(cudaStream_t stream_ptr) {
|
|
256
271
|
// need lock ouside this function
|
|
257
272
|
if (!HasArena(stream_ptr)) return;
|
|
258
273
|
|
|
@@ -266,22 +281,17 @@ void SingleDeviceMemoryPool::CompactIndex(cudaStream_t stream_ptr, bool free) {
|
|
|
266
281
|
if (free_list.empty()) {
|
|
267
282
|
continue;
|
|
268
283
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
keep_list.emplace_back(chunk);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (keep_list.size() == 0) {
|
|
277
|
-
continue;
|
|
284
|
+
FreeList keep_list;
|
|
285
|
+
for (auto chunk : free_list) {
|
|
286
|
+
if (chunk->prev() != nullptr || chunk->next() != nullptr) {
|
|
287
|
+
keep_list.emplace_back(chunk);
|
|
278
288
|
}
|
|
279
|
-
new_arena_index_map.emplace_back(arena_index_map[arena_index]);
|
|
280
|
-
new_arena.emplace_back(keep_list);
|
|
281
|
-
} else {
|
|
282
|
-
new_arena_index_map.emplace_back(arena_index_map[arena_index]);
|
|
283
|
-
new_arena.emplace_back(free_list);
|
|
284
289
|
}
|
|
290
|
+
if (keep_list.size() == 0) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
new_arena_index_map.emplace_back(arena_index_map[arena_index]);
|
|
294
|
+
new_arena.emplace_back(keep_list);
|
|
285
295
|
}
|
|
286
296
|
if (new_arena.empty()) {
|
|
287
297
|
index_.erase(stream_ptr);
|
|
@@ -299,7 +309,7 @@ void SingleDeviceMemoryPool::FreeAllBlocks() {
|
|
|
299
309
|
std::vector<cudaStream_t> keys(free_.size());
|
|
300
310
|
transform(free_.begin(), free_.end(), keys.begin(), [](auto pair) { return pair.first; });
|
|
301
311
|
for (cudaStream_t stream_ptr : keys) {
|
|
302
|
-
CompactIndex(stream_ptr
|
|
312
|
+
CompactIndex(stream_ptr);
|
|
303
313
|
}
|
|
304
314
|
}
|
|
305
315
|
|
|
@@ -307,7 +317,7 @@ void SingleDeviceMemoryPool::FreeAllBlocks() {
|
|
|
307
317
|
void SingleDeviceMemoryPool::FreeAllBlocks(cudaStream_t stream_ptr) {
|
|
308
318
|
std::lock_guard<std::recursive_mutex> lock{mutex_};
|
|
309
319
|
|
|
310
|
-
CompactIndex(stream_ptr
|
|
320
|
+
CompactIndex(stream_ptr);
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
size_t SingleDeviceMemoryPool::GetNumFreeBlocks() {
|
|
@@ -284,7 +284,9 @@ public:
|
|
|
284
284
|
// be found in the free list as the chunk is allocated.)
|
|
285
285
|
bool RemoveFromFreeList(size_t size, std::shared_ptr<Chunk>& chunk, cudaStream_t stream_ptr);
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
// Drops every chunk that was never split from a larger one, and every
|
|
288
|
+
// bin left empty by that.
|
|
289
|
+
void CompactIndex(cudaStream_t stream_ptr);
|
|
288
290
|
};
|
|
289
291
|
|
|
290
292
|
// Memory pool for all GPU devices on the host.
|
|
@@ -337,10 +337,10 @@ public:
|
|
|
337
337
|
assert(pool_->GetFreeBytes() == kRoundSize * 8);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
// RemoveFromFreeList
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
340
|
+
// RemoveFromFreeList drops a bin its erase leaves empty, the same as
|
|
341
|
+
// Malloc. The merge path below Free empties one whenever it takes a
|
|
342
|
+
// neighbour out, so a bin left behind here would be walked by every later
|
|
343
|
+
// search.
|
|
344
344
|
void TestRemoveFromFreeList() {
|
|
345
345
|
Arena& arena = pool_->GetArena(stream_ptr_);
|
|
346
346
|
ArenaIndexMap& arena_index_map = pool_->GetArenaIndexMap(stream_ptr_);
|
|
@@ -372,36 +372,30 @@ public:
|
|
|
372
372
|
assert(arena_index_map[1] == 3);
|
|
373
373
|
assert(arena_index_map[2] == 4);
|
|
374
374
|
|
|
375
|
-
// remove two from two
|
|
375
|
+
// remove two from two, which empties that bin
|
|
376
376
|
pool_->RemoveFromFreeList(chunk2->size(), chunk2, stream_ptr_);
|
|
377
|
-
assert(arena.size() ==
|
|
377
|
+
assert(arena.size() == 2);
|
|
378
378
|
assert(arena[0].size() == 1);
|
|
379
|
-
assert(arena[1].size() ==
|
|
380
|
-
assert(
|
|
381
|
-
assert(arena_index_map.size() == 3);
|
|
379
|
+
assert(arena[1].size() == 1);
|
|
380
|
+
assert(arena_index_map.size() == 2);
|
|
382
381
|
assert(arena_index_map[0] == 2);
|
|
383
|
-
assert(arena_index_map[1] ==
|
|
384
|
-
assert(arena_index_map[2] == 4);
|
|
382
|
+
assert(arena_index_map[1] == 4);
|
|
385
383
|
|
|
386
384
|
pool_->RemoveFromFreeList(chunk3->size(), chunk3, stream_ptr_);
|
|
387
|
-
assert(arena.size() ==
|
|
385
|
+
assert(arena.size() == 1);
|
|
388
386
|
assert(arena[0].size() == 1);
|
|
389
|
-
assert(
|
|
390
|
-
assert(arena[2].size() == 0);
|
|
391
|
-
assert(arena_index_map.size() == 3);
|
|
387
|
+
assert(arena_index_map.size() == 1);
|
|
392
388
|
assert(arena_index_map[0] == 2);
|
|
393
|
-
assert(arena_index_map[1] == 3);
|
|
394
|
-
assert(arena_index_map[2] == 4);
|
|
395
389
|
|
|
396
390
|
pool_->RemoveFromFreeList(chunk4->size(), chunk4, stream_ptr_);
|
|
397
|
-
assert(arena.size() ==
|
|
398
|
-
assert(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
assert(
|
|
403
|
-
assert(
|
|
404
|
-
assert(
|
|
391
|
+
assert(arena.size() == 0);
|
|
392
|
+
assert(arena_index_map.size() == 0);
|
|
393
|
+
|
|
394
|
+
// A chunk that is not in the free list leaves the arena alone.
|
|
395
|
+
pool_->AppendToFreeList(chunk1->size(), chunk1, stream_ptr_);
|
|
396
|
+
assert(!pool_->RemoveFromFreeList(chunk2->size(), chunk2, stream_ptr_));
|
|
397
|
+
assert(arena.size() == 1);
|
|
398
|
+
assert(arena[0].size() == 1);
|
|
405
399
|
}
|
|
406
400
|
|
|
407
401
|
void TestMalloc() {
|
|
@@ -11,10 +11,18 @@ extern "C" {
|
|
|
11
11
|
#endif
|
|
12
12
|
#endif
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Two of these often sit side by side, so the message has to say which array
|
|
15
|
+
// was turned down as well as what it is.
|
|
16
|
+
static inline void
|
|
17
|
+
cumo_na_check_narray_type(VALUE x, VALUE type, const char *name)
|
|
18
|
+
{
|
|
19
|
+
if (rb_obj_class(x) != type) {
|
|
20
|
+
rb_raise(rb_eTypeError, "%s must be %" PRIsVALUE ", not %" PRIsVALUE,
|
|
21
|
+
name, type, rb_obj_class(x));
|
|
17
22
|
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#define CUMO_CHECK_NARRAY_TYPE(x,t,name) cumo_na_check_narray_type((x),(t),(name))
|
|
18
26
|
|
|
19
27
|
#define CUMO_CHECK_SIZE_EQ(sz1,sz2) \
|
|
20
28
|
if ((sz1) != (sz2)) { \
|
|
@@ -38,7 +38,7 @@ cumo_cuda_cudnn_check_output(VALUE out, VALUE type, size_t ndim, size_t *shape)
|
|
|
38
38
|
{
|
|
39
39
|
cumo_narray_t *na;
|
|
40
40
|
|
|
41
|
-
CUMO_CHECK_NARRAY_TYPE(out, type);
|
|
41
|
+
CUMO_CHECK_NARRAY_TYPE(out, type, "output");
|
|
42
42
|
CumoGetNArray(out, na);
|
|
43
43
|
CUMO_CHECK_DIM_EQ((size_t)(na->ndim), ndim);
|
|
44
44
|
for (size_t idim = 0; idim < ndim; ++idim) {
|
|
@@ -75,7 +75,7 @@ cumo_cuda_cudnn_check_input(VALUE in, VALUE type, size_t ndim, size_t *shape)
|
|
|
75
75
|
{
|
|
76
76
|
cumo_narray_t *na;
|
|
77
77
|
|
|
78
|
-
CUMO_CHECK_NARRAY_TYPE(in, type);
|
|
78
|
+
CUMO_CHECK_NARRAY_TYPE(in, type, "input");
|
|
79
79
|
CumoGetNArray(in, na);
|
|
80
80
|
CUMO_CHECK_DIM_EQ((size_t)(na->ndim), ndim);
|
|
81
81
|
for (size_t idim = 0; idim < ndim; ++idim) {
|
|
@@ -88,10 +88,7 @@ cumo_cuda_cudnn_check_input(VALUE in, VALUE type, size_t ndim, size_t *shape)
|
|
|
88
88
|
static inline void
|
|
89
89
|
cumo_cuda_cudnn_check_param_type(VALUE param, VALUE type, const char* name)
|
|
90
90
|
{
|
|
91
|
-
|
|
92
|
-
rb_raise(rb_eTypeError, "%s must be %s, not %s",
|
|
93
|
-
name, rb_class2name(type), rb_obj_classname(param));
|
|
94
|
-
}
|
|
91
|
+
CUMO_CHECK_NARRAY_TYPE(param, type, name);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
void
|
|
@@ -176,6 +176,22 @@ cumo_na_make_iarray_stridx(cumo_na_loop_args_t* arg)
|
|
|
176
176
|
return iarray;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Whether any argument of the loop is backed by an index array. With
|
|
180
|
+
// INDEX_LOOP an operand can arrive carrying one, and cumo_na_iarray_t holds
|
|
181
|
+
// byte steps only: an indexed dimension has a step of zero there, so a loop
|
|
182
|
+
// that answers yes has to address its operands through the stridx form above.
|
|
183
|
+
static int
|
|
184
|
+
cumo_na_loop_has_index(cumo_na_loop_t *const lp)
|
|
185
|
+
{
|
|
186
|
+
int j, i;
|
|
187
|
+
for (j = 0; j < lp->narg; ++j) {
|
|
188
|
+
for (i = 0; i < lp->args[j].ndim; ++i) {
|
|
189
|
+
if (lp->args[j].iter[i].idx) return 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
179
195
|
static cumo_na_bit_iarray_stridx_t
|
|
180
196
|
cumo_na_make_bit_iarray_stridx(cumo_na_loop_args_t* arg)
|
|
181
197
|
{
|
|
@@ -54,6 +54,8 @@ void cumo_na_release_lock(VALUE); // currently do nothing
|
|
|
54
54
|
// used in reduce methods
|
|
55
55
|
VALUE cumo_na_reduce_dimension(int argc, VALUE *argv, int naryc, VALUE *naryv,
|
|
56
56
|
cumo_ndfunc_t *ndf, cumo_na_iter_func_t nan_iter);
|
|
57
|
+
VALUE cumo_na_get_reduce(VALUE v);
|
|
58
|
+
void cumo_na_set_reduce(VALUE v, VALUE reduce);
|
|
57
59
|
|
|
58
60
|
VALUE cumo_na_reduce_options(VALUE axes, VALUE *opts, int naryc, VALUE *naryv,
|
|
59
61
|
cumo_ndfunc_t *ndf);
|