cumo 0.5.11 → 0.7.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 +76 -1
- data/README.md +133 -0
- data/bench/cumo_shape_probe.rb +264 -0
- data/bench/cumo_sync_probe.rb +71 -8
- data/ext/cumo/cuda/cublas.c +0 -40
- data/ext/cumo/cuda/cudnn.c +48 -0
- data/ext/cumo/cuda/cudnn_impl.cpp +100 -63
- data/ext/cumo/cuda/memory_pool_impl_test.cpp +4 -1
- data/ext/cumo/cuda/nvrtc.c +1 -1
- data/ext/cumo/cuda/runtime.c +3 -4
- data/ext/cumo/cumo.c +34 -0
- data/ext/cumo/depend.erb +7 -0
- data/ext/cumo/extconf.rb +2 -0
- data/ext/cumo/include/cumo/bit_reduce_kernel.h +119 -13
- data/ext/cumo/include/cumo/check.h +51 -0
- data/ext/cumo/include/cumo/cuda/cudnn.h +47 -43
- data/ext/cumo/include/cumo/cuda/cumo_thrust.hpp +1 -0
- data/ext/cumo/include/cumo/cuda/runtime.h +28 -0
- data/ext/cumo/include/cumo/indexer.h +29 -8
- data/ext/cumo/include/cumo/intern.h +4 -1
- data/ext/cumo/include/cumo/narray.h +69 -3
- data/ext/cumo/include/cumo/ndloop.h +1 -0
- data/ext/cumo/include/cumo/reduce_kernel.h +133 -50
- data/ext/cumo/include/cumo/template.h +35 -10
- data/ext/cumo/include/cumo/template_kernel.h +96 -0
- data/ext/cumo/include/cumo/types/bit.h +3 -0
- data/ext/cumo/include/cumo/types/bit_kernel.h +3 -0
- data/ext/cumo/include/cumo/types/complex_macro.h +2 -0
- data/ext/cumo/include/cumo/types/complex_macro_kernel.h +2 -0
- data/ext/cumo/include/cumo/types/float_macro.h +17 -2
- data/ext/cumo/include/cumo/types/float_macro_kernel.h +17 -2
- data/ext/cumo/include/cumo/types/half_def.h +129 -0
- data/ext/cumo/include/cumo/types/half_def_kernel.h +28 -0
- data/ext/cumo/include/cumo/types/half_macro.h +188 -0
- data/ext/cumo/include/cumo/types/half_macro_kernel.h +198 -0
- data/ext/cumo/include/cumo/types/hfloat.h +22 -0
- data/ext/cumo/include/cumo/types/hfloat_kernel.h +16 -0
- data/ext/cumo/include/cumo/types/int_macro.h +20 -0
- data/ext/cumo/include/cumo/types/int_macro_kernel.h +20 -0
- data/ext/cumo/include/cumo/types/real_accum.h +2 -0
- data/ext/cumo/include/cumo/types/real_accum_kernel.h +2 -0
- data/ext/cumo/include/cumo/types/robj_macro.h +11 -3
- data/ext/cumo/include/cumo/types/xint_macro.h +3 -0
- data/ext/cumo/include/cumo/types/xint_macro_kernel.h +2 -0
- data/ext/cumo/include/cumo.h +2 -2
- data/ext/cumo/narray/data.c +70 -199
- data/ext/cumo/narray/data_kernel.cu +113 -0
- data/ext/cumo/narray/gen/def/bit.rb +2 -0
- data/ext/cumo/narray/gen/def/dcomplex.rb +2 -0
- data/ext/cumo/narray/gen/def/dfloat.rb +8 -0
- data/ext/cumo/narray/gen/def/hfloat.rb +47 -0
- data/ext/cumo/narray/gen/def/int16.rb +2 -0
- data/ext/cumo/narray/gen/def/int32.rb +2 -0
- data/ext/cumo/narray/gen/def/int64.rb +2 -0
- data/ext/cumo/narray/gen/def/int8.rb +2 -0
- data/ext/cumo/narray/gen/def/robject.rb +2 -0
- data/ext/cumo/narray/gen/def/scomplex.rb +2 -0
- data/ext/cumo/narray/gen/def/sfloat.rb +8 -0
- data/ext/cumo/narray/gen/def/uint16.rb +3 -1
- data/ext/cumo/narray/gen/def/uint32.rb +3 -1
- data/ext/cumo/narray/gen/def/uint64.rb +2 -0
- data/ext/cumo/narray/gen/def/uint8.rb +3 -1
- data/ext/cumo/narray/gen/erbpp2.rb +6 -0
- data/ext/cumo/narray/gen/narray_def.rb +5 -0
- data/ext/cumo/narray/gen/spec.rb +11 -0
- data/ext/cumo/narray/gen/tmpl/accum.c +1 -1
- data/ext/cumo/narray/gen/tmpl/accum_arg.c +1 -1
- data/ext/cumo/narray/gen/tmpl/accum_arg_kernel.cu +5 -5
- data/ext/cumo/narray/gen/tmpl/accum_binary_kernel.cu +14 -1
- data/ext/cumo/narray/gen/tmpl/accum_index.c +1 -1
- data/ext/cumo/narray/gen/tmpl/accum_index_kernel.cu +5 -5
- data/ext/cumo/narray/gen/tmpl/alloc_func.c +4 -7
- data/ext/cumo/narray/gen/tmpl/allocate.c +1 -2
- data/ext/cumo/narray/gen/tmpl/aref_cpu.c +1 -1
- data/ext/cumo/narray/gen/tmpl/batch_norm.c +27 -36
- data/ext/cumo/narray/gen/tmpl/batch_norm_backward.c +26 -35
- data/ext/cumo/narray/gen/tmpl/binary.c +3 -3
- data/ext/cumo/narray/gen/tmpl/binary2.c +1 -1
- data/ext/cumo/narray/gen/tmpl/binary_kernel.cu +69 -2
- data/ext/cumo/narray/gen/tmpl/binary_s.c +3 -3
- data/ext/cumo/narray/gen/tmpl/clip.c +9 -9
- data/ext/cumo/narray/gen/tmpl/cond_binary.c +3 -3
- data/ext/cumo/narray/gen/tmpl/cond_unary.c +2 -2
- data/ext/cumo/narray/gen/tmpl/conv.c +19 -25
- data/ext/cumo/narray/gen/tmpl/conv_grad_w.c +21 -27
- data/ext/cumo/narray/gen/tmpl/conv_transpose.c +21 -27
- data/ext/cumo/narray/gen/tmpl/cum.c +24 -1
- data/ext/cumo/narray/gen/tmpl/cum_kernel.cu +29 -4
- data/ext/cumo/narray/gen/tmpl/each.c +2 -3
- data/ext/cumo/narray/gen/tmpl/each_with_index.c +5 -21
- data/ext/cumo/narray/gen/tmpl/ewcomp.c +1 -1
- data/ext/cumo/narray/gen/tmpl/extract_cpu.c +1 -1
- data/ext/cumo/narray/gen/tmpl/extract_data.c +1 -1
- data/ext/cumo/narray/gen/tmpl/fill.c +2 -2
- data/ext/cumo/narray/gen/tmpl/fixed_batch_norm.c +18 -27
- data/ext/cumo/narray/gen/tmpl/float_accum_kernel.cu +42 -37
- data/ext/cumo/narray/gen/tmpl/format.c +1 -1
- data/ext/cumo/narray/gen/tmpl/format_to_a.c +1 -1
- data/ext/cumo/narray/gen/tmpl/frexp.c +1 -1
- data/ext/cumo/narray/gen/tmpl/gemm.c +78 -27
- data/ext/cumo/narray/gen/tmpl/init_class.c +0 -7
- data/ext/cumo/narray/gen/tmpl/init_upcast.c +6 -0
- data/ext/cumo/narray/gen/tmpl/inspect.c +1 -1
- data/ext/cumo/narray/gen/tmpl/lib.c +11 -1
- data/ext/cumo/narray/gen/tmpl/map_with_index.c +7 -14
- data/ext/cumo/narray/gen/tmpl/median.c +9 -47
- data/ext/cumo/narray/gen/tmpl/minmax.c +1 -1
- data/ext/cumo/narray/gen/tmpl/poly.c +2 -2
- data/ext/cumo/narray/gen/tmpl/pooling_backward.c +6 -18
- data/ext/cumo/narray/gen/tmpl/pooling_forward.c +6 -18
- data/ext/cumo/narray/gen/tmpl/pow.c +6 -6
- data/ext/cumo/narray/gen/tmpl/qsort.c +0 -1
- data/ext/cumo/narray/gen/tmpl/rand.c +20 -2
- data/ext/cumo/narray/gen/tmpl/rand_kernel.cu +13 -0
- data/ext/cumo/narray/gen/tmpl/rand_norm.c +4 -4
- data/ext/cumo/narray/gen/tmpl/rand_norm_kernel.cu +7 -5
- data/ext/cumo/narray/gen/tmpl/real_accum_kernel.cu +155 -96
- data/ext/cumo/narray/gen/tmpl/set2.c +1 -1
- data/ext/cumo/narray/gen/tmpl/sort.c +1 -1
- data/ext/cumo/narray/gen/tmpl/sort_index.c +6 -3
- data/ext/cumo/narray/gen/tmpl/store_array.c +21 -22
- data/ext/cumo/narray/gen/tmpl/store_bit.c +2 -2
- data/ext/cumo/narray/gen/tmpl/store_from.c +2 -2
- data/ext/cumo/narray/gen/tmpl/store_from_kernel.cu +37 -2
- data/ext/cumo/narray/gen/tmpl/to_a.c +1 -1
- data/ext/cumo/narray/gen/tmpl/unary.c +21 -4
- data/ext/cumo/narray/gen/tmpl/unary2.c +2 -2
- data/ext/cumo/narray/gen/tmpl/unary_ret2.c +1 -1
- data/ext/cumo/narray/gen/tmpl/unary_s.c +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/allocate.c +2 -3
- data/ext/cumo/narray/gen/tmpl_bit/aref_cpu.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/binary.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/bit_count_cpu.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/bit_reduce.c +4 -1
- data/ext/cumo/narray/gen/tmpl_bit/bit_stat.c +36 -0
- data/ext/cumo/narray/gen/tmpl_bit/bit_stat_kernel.cu +4 -0
- data/ext/cumo/narray/gen/tmpl_bit/each.c +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/each_with_index.c +5 -20
- data/ext/cumo/narray/gen/tmpl_bit/extract.c +13 -27
- data/ext/cumo/narray/gen/tmpl_bit/extract_cpu.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/fill.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/format.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/format_to_a.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/inspect.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/mask.c +38 -17
- data/ext/cumo/narray/gen/tmpl_bit/mask_kernel.cu +4 -4
- data/ext/cumo/narray/gen/tmpl_bit/store_array.c +10 -11
- data/ext/cumo/narray/gen/tmpl_bit/store_bit.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/store_from.c +2 -2
- data/ext/cumo/narray/gen/tmpl_bit/to_a.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/unary.c +1 -1
- data/ext/cumo/narray/gen/tmpl_bit/where.c +66 -13
- data/ext/cumo/narray/gen/tmpl_bit/where2.c +57 -20
- data/ext/cumo/narray/gen/tmpl_bit/where_kernel.cu +6 -6
- data/ext/cumo/narray/index.c +83 -26
- data/ext/cumo/narray/index_kernel.cu +15 -0
- data/ext/cumo/narray/math.c +2 -0
- data/ext/cumo/narray/narray.c +421 -178
- data/ext/cumo/narray/ndloop.c +158 -26
- data/ext/cumo/narray/sort_kernel.cu +39 -18
- data/ext/cumo/narray/struct.c +14 -17
- data/lib/cumo/cuda/cudnn.rb +1 -1
- data/lib/cumo/narray/extra.rb +130 -26
- data/test/bit_test.rb +290 -0
- data/test/cudnn_test.rb +208 -34
- data/test/cumo_test.rb +1 -0
- data/test/extra_test.rb +75 -7
- data/test/hfloat_test.rb +717 -0
- data/test/math_test.rb +4 -1
- data/test/narray_alt_coverage_test.rb +7 -0
- data/test/narray_test.rb +1865 -10
- data/test/test_helper.rb +73 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97c7f575209616cb9fa484e5dd4096bac11170aef18e99f1f6f733e599daa9af
|
|
4
|
+
data.tar.gz: 5efabd7aface13fac85dcef81fef54dd9bb22d3748666177ff221741a8419486
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 314d2b224cb753761618b6bc833af4715d8d1cb2e1fd8de05b0ad65483ec940c31167997abe7cc76e2f056b12b442885731d8e6b4d4b47ec174c19c70c2326e9
|
|
7
|
+
data.tar.gz: 5a4909d952dbe926b52ccaf04010b3ffcf08617a2af710ff49a70764ff7e626ffddcef04ebcb4ca6b5aab8c7b14170b9b3328768a203dd7961d2fbab5c973002
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
# 0.7.0 (2026/09/13)
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
* `cumo_narray_data_t` no longer carries `owned` and `cumo_narray_t` no longer carries `elmsz`. Both ship in `narray.h`, so an extension compiled against 0.6.0 has to be rebuilt. `NArray#free` also refuses a frozen array, as the other release paths already did (PR #421)
|
|
6
|
+
* `gemm` raises `RangeError` for the sizes cuBLAS takes as `int`, where they wrapped and the call read outside its operands (PR #399)
|
|
7
|
+
* Taking a pointer refuses an `allocate` that leaves the array without a buffer, or hands back one already holding data, and `to_binary` measures the array by its type rather than by the `ELEMENT_BYTE_SIZE` its class names (PR #393, PR #392)
|
|
8
|
+
* `store_binary` refuses a non-contiguous view, and a `Cumo::Bit` view that does not begin and end on a byte. It copies the string it is given, so `NArray#free` answers true after a frozen one where it answered false (PR #390, PR #389)
|
|
9
|
+
* `Cumo::Int8.upcast(Cumo::UInt8)` answers `Cumo::Int16` where it answered `Cumo::Int8`, and `Cumo::Bit.upcast(Cumo::RObject)` answers rather than `nil`, the tables having been built before the classes they name existed (PR #378)
|
|
10
|
+
|
|
11
|
+
Fixes:
|
|
12
|
+
|
|
13
|
+
* Fix a view constructor that raises partway leaving `stridx` unreadable for the free hook, and leaking the index array it had just built (PR #407)
|
|
14
|
+
* Fix `Cumo::Bit#each`, `map` and `map_with_index` reading what the block queued before the device had run it, 7 of the 18 dtype and method pairs having answered differently from numo (PR #401)
|
|
15
|
+
* Fix `gemm` reading and writing far outside its operands when a matrix holds more than 2**31 elements, the batch strides reaching cuBLAS as `int` where cuBLAS takes `long long int` (PR #399)
|
|
16
|
+
* Fix `median(nan: true)` answering with a number when a NaN is present, the host quicksort it used leaving NaN unordered (PR #398)
|
|
17
|
+
* Fix `where`, `where2` and a masked `aref` writing past the index array they sized, `allocate` being free to resize it between the count and the walk (PR #397)
|
|
18
|
+
* Fix `Cumo::Bit#each_with_index` writing in front of the counter array on a zero-dimensional array, which ordinary code reaches since indexing a Bit answers a zero-dimensional Bit (PR #396)
|
|
19
|
+
* Fix the segfaults an `allocate` that takes no buffer, reshapes the array, or claims its own element size leaves behind, and `to_binary` handing back a 16KB String from a 32 byte allocation (PR #393, PR #392)
|
|
20
|
+
* Fix `store_binary` writing at the start of the base rather than through the view, and pointing the array at a frozen String's bytes, which a compaction moves out from under it (PR #390, PR #389)
|
|
21
|
+
* Fix storing a Ruby Array of sub-narrays: the rows came back as the first one repeated, a row short in an outer axis copied past its own end, and a row with no elements stored the loop's flag as a value, 8.5us a row down to 1.6 (PR #384, PR #383, PR #382)
|
|
22
|
+
* Fix flattening a `Cumo::Bit` view that walks an axis backwards reading near address zero, which `to_a`, `where`, `where2` and `mask` all reach (PR #380)
|
|
23
|
+
* Fix a length-1 axis costing a view its contiguity, and `reshape!` on one carrying a negative stride walking out of the allocation; of 288 views measured, 92 more answer `contiguous?` and none fewer (PR #379, PR #376)
|
|
24
|
+
* Fix `:new` in front of an index-backed dimension reading and writing outside the array, only a trailing new axis having been recognized as one (PR #375)
|
|
25
|
+
* Fix `min`, `max`, `minmax` and `ptp` answering a finite number for an array of infinities, the nan-aware forms seeding with the largest finite value the dtype holds (PR #372)
|
|
26
|
+
|
|
27
|
+
Changes:
|
|
28
|
+
|
|
29
|
+
* Add `Cumo::HFloat`, the 16-bit float dtype the GPU has had since Maxwell: the four operations, the comparisons, the reductions, `Cumo::NMath`, `sort`, `rand`, `dot` through cuBLAS with a float compute type, and the cuDNN methods, which now reach the tensor cores. A 1024x1024 `dot` fell to the mulsum fallback and was 285x slower before it, and `cumsum` stays undefined for half until the scan carries a wider accumulator (issue #107, PR #371, PR #370, PR #368, PR #367, PR #366, PR #365)
|
|
30
|
+
* Copy a transposed 2-d view, and run an elementwise op with a transposed operand, through a 32x32 shared memory tile; on 16M SFloat the copy goes from 177 to 380 GB/s and the op from 247 to 396 GB/s (PR #416, PR #415)
|
|
31
|
+
* Point a derived view at the index arrays it would have copied, one device allocation and one copy per dimension, and let `reverse` and `diagonal` borrow the dimensions they only pass through; on a 2**20 index `transpose` goes from 394us to 0.3us, `reverse(1)` from 327us to 0.2us and `diagonal` from 329us to 0.1us (PR #412, PR #410)
|
|
32
|
+
* Let any device synchronize settle a view's index rather than only the one the index code makes, and record the epoch in every constructor that fills an index; storing sub-narrays into an index-backed view goes from 11.7-18.0us to 5.9-8.9us, and a reversed, expanded or diagonal index view stops waiting once per read (PR #414, PR #388)
|
|
33
|
+
* Wait for a sub-narray index only when the row binds one, rather than whenever any argument uses one; a store into an index-backed destination goes from 6.3us to 1.7us a row (PR #387)
|
|
34
|
+
* Reverse a view's index array with a kernel instead of a host loop that stopped the whole device first; a 2**20 index turns around in 480us rather than 1600us (PR #406)
|
|
35
|
+
* Wait for the device once per row of a host sort instead of once per recursion, a 20000 element row having cost 4003 waits; over 200000 elements `sort(nan: true)` goes from 18.95 to 13.03 ms, `median` from 18.15 to 12.41 and `sort_index` from 20.99 to 14.39 (PR #394)
|
|
36
|
+
* Warn when a view is built holding an index array that nothing owns, which leaks it when that view is the last reference and dangles when it is not (PR #413)
|
|
37
|
+
* Say once what the duplicated templates and kernels had each grown a copy of: the transpose tile walk, the `with_index` block argument and its 0-dimensional clamp, the Bit pointer rebase rule, the min and max rules, the minmax pair, and the byte size a packed bit array takes (PR #422, PR #418, PR #409, PR #408, PR #381, PR #377, PR #374, PR #373)
|
|
38
|
+
* Carry the sub-narray reach per dimension and through the row's own shape rather than through the value channel the store templates read (PR #386, PR #385)
|
|
39
|
+
* Separate the cuDNN scalar, compute and parameter types from the tensor's dtype, so a dtype whose compute type differs is named in `gen/def` rather than in eight templates (PR #369)
|
|
40
|
+
* Delete the `dot` and cuBLAS option code no build compiles, the branch that called a view with no strides contiguous, the second spelling of whether a view's offset reaches a char pointer, and the wait in front of a blocking copy that already queues behind the fill it waited for (PR #417, PR #411, PR #404, PR #403)
|
|
41
|
+
* Count the waits a call makes in the sync probe and probe the block walks, run `Cumo::HFloat` through the structural, math and alt-coverage suites, skip the boundary tests on a machine too small for them, and name the message the gemm row count tests are there for (PR #420, PR #419, PR #405, PR #402, PR #400)
|
|
42
|
+
|
|
43
|
+
# 0.6.0 (2026/09/06)
|
|
44
|
+
|
|
45
|
+
Breaking changes:
|
|
46
|
+
|
|
47
|
+
* `divmod` and `%` floor the quotient and give the remainder the divisor's sign, as Ruby does, on every signed type: `Cumo::Int32[-7].divmod(3)` answers `[-3, 2]` where it answered `[-2, -1]`, and `-7 % 3` answers `2` rather than `-1`. Dividing a float by zero answers a quotient of `NaN` where it answered `Infinity`. Unsigned types are unchanged (PR #364)
|
|
48
|
+
* A view whose base has since been given a smaller shape raises `RuntimeError` instead of reading past what the base holds (PR #363)
|
|
49
|
+
* `marshal_load` and `initialize` leave an array unallocated when they take a new shape, so it reads as unallocated until something is stored in it, and both refuse a view. `initialize` also refuses a frozen array, as `marshal_load` already did (PR #361, PR #360, PR #355)
|
|
50
|
+
* A range subscript longer than `2**31` answers the length it names instead of an empty view, and one longer than `2**32` no longer answers with the low 32 bits of it (PR #354)
|
|
51
|
+
* `cov` rejects a 3-dimensional receiver or `y`, 2-dimensional or non-integer `fweights`, and any `ddof` but 0 or 1, where it answered from them; it warns and answers `nan` when the degrees of freedom come out at zero or below (PR #343)
|
|
52
|
+
* `Cumo::Bit#extract` answers with a zero-dimensional `Cumo::Bit`, as every other dtype does, rather than a Ruby Integer read back through a synchronize (PR #341)
|
|
53
|
+
* `batch_norm` and its backward reject an axis that names nothing in `x`, repeats a dimension, or is out of order, where they answered as though a different axis had been given (PR #336)
|
|
54
|
+
|
|
55
|
+
Fixes:
|
|
56
|
+
|
|
57
|
+
* Fix `RObject#divmod` reading the pair an element's `divmod` answered without looking at it: a bare Integer or a String took the process down, and a shorter Array handed back the words past its end as the quotient and the remainder (PR #359)
|
|
58
|
+
* Fix an out-of-bounds read when `initialize` is given a new shape through `send`, which left the buffer sized for the old one: 4096 elements were read out of 8, and the same on an `Cumo::RObject` crashed at the next collection (PR #361)
|
|
59
|
+
* Fix a memory leak when an Array subscript is rejected, the host buffer it is staged in being freed only after the loop that fills it, and the memory a subscript piles up before the collector takes it back (PR #357, PR #358)
|
|
60
|
+
* Fix `marshal_load` writing past a buffer sized for the shape it replaces, and the double free that came of releasing a buffer noted down before the shape's own `to_int` had run (PR #355, PR #356)
|
|
61
|
+
* Fix reading past a Ruby array or string that a conversion shrank underneath the walk: a subscript, a shape, an axis list, a marshal array and the string behind `from_binary` were each measured once and walked afterwards, and `to_int` is free to empty any of them (PR #353)
|
|
62
|
+
* Fix `Cumo::Bit#swap_byte` walking one byte per bit, eight times past the end of the packed buffer; `hton`, `to_network` and `to_swapped` reach the same loop (PR #352)
|
|
63
|
+
|
|
64
|
+
Changes:
|
|
65
|
+
|
|
66
|
+
* Add `argsort`, which answers positions along an axis rather than into the flattened array as `sort_index` does (PR #342)
|
|
67
|
+
* Add `mean`, `var`, `stddev` and `rms` to the integer types and to `Cumo::Bit`, which had only the float ones (PR #344, PR #345)
|
|
68
|
+
* Walk an elementwise function along the memory of the operand it writes rather than the shape it was given; on a transposed 1024x1024 SFloat view `a.inplace + 1.0` goes from 1180.6us to 78.3us (PR #351)
|
|
69
|
+
* Reduce along the contiguous axis with a contiguous group of threads, keep a reduction's operands out of local memory, and give a short strided row more elements per thread; `[1024, 4, 1024].sum(axis: 1)` goes from 142.0us to 23.4us and the same `mulsum` from 675.3us to 30.9us (PR #350, PR #347)
|
|
70
|
+
* Split a flat index in 32 bits where the array fits in them, the 64-bit division being a software routine on the GPU; a 4M SFloat `a[true, 1..-2] * 2.0` goes from 60.6us to 38.7us (PR #348)
|
|
71
|
+
* Reverse the bytes of an array in a kernel instead of on the host; a 4M SFloat `swap_byte` goes from 5617.0us to 13.0us (PR #335)
|
|
72
|
+
* Add `bench/cumo_shape_probe.rb`, which reads each shape of an operation against the same bytes moved along the best path it has (PR #349)
|
|
73
|
+
* Make every object depend on the headers it is built from, a header-only change having left stale objects behind (PR #346)
|
|
74
|
+
* Answer the four cuDNN TODOs left in the tree, and say what the memory pool's `TestRemoveFromFreeList` holds rather than what it waits for (PR #340, PR #339, PR #338, PR #337)
|
|
75
|
+
|
|
1
76
|
# 0.5.11 (2026/08/29)
|
|
2
77
|
|
|
3
78
|
Breaking changes:
|
|
@@ -232,7 +307,7 @@ Fixes:
|
|
|
232
307
|
* Backport: improve discrimination between int32/64
|
|
233
308
|
* Backport: missing declaration of id_ge
|
|
234
309
|
* Backport: support casting any object that responds to 'to_a'
|
|
235
|
-
* Backport: fix the range of int32 as
|
|
310
|
+
* Backport: fix the range of int32 as `-2**31 .. 2**31-1`
|
|
236
311
|
* Backport: if int32_max is Bignum, it should be protected from GC
|
|
237
312
|
* fix: handle compiler flags included in RbConfig::CONFIG properly (#157)
|
|
238
313
|
* Backport: Added more percentile tests
|
data/README.md
CHANGED
|
@@ -222,6 +222,126 @@ Cumo::SFloat::Math.atan2(a, 2.0) #=> Cumo::SFloat
|
|
|
222
222
|
|
|
223
223
|
The 0-dimensional form has no effect under Numo, where `[]` returns a Ruby Float.
|
|
224
224
|
|
|
225
|
+
### Half Precision
|
|
226
|
+
|
|
227
|
+
`Cumo::HFloat`, also reachable as `Cumo::Float16`, holds IEEE binary16: one sign bit, five of exponent and ten of mantissa.
|
|
228
|
+
It exists for the two things half is good at, moving half as many bytes and reaching the tensor cores, and it promotes exactly as `Cumo::SFloat` does, so an integer array or a Ruby Float mixed into an expression stays half while anything wider takes over.
|
|
229
|
+
|
|
230
|
+
What it cannot hold is the thing to plan around.
|
|
231
|
+
Integers are exact only to 2048, and the largest finite value is 65504:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
Cumo::HFloat[2049.0] #=> 2048.0
|
|
235
|
+
Cumo::HFloat[50257.0] #=> 50272.0
|
|
236
|
+
Cumo::HFloat[100000] #=> Infinity
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
There is no exception on the way past the top; the value saturates, as it does in every other float type.
|
|
240
|
+
Anything that carries an index rather than a measurement has to be built in a wider type and cast afterwards.
|
|
241
|
+
|
|
242
|
+
Reductions do not inherit that limit, because they accumulate in single precision and round once at the end.
|
|
243
|
+
A sum of forty thousand ones is forty thousand, not the 2048 a half accumulator would stop at:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Cumo::HFloat.new(40_000).fill(1.0).sum #=> 40000.0
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`sum`, `mean`, `var`, `stddev`, `rms`, `mulsum`, `dot`, `gemm` and `cumsum` all widen this way.
|
|
250
|
+
`prod` and `cumprod` do not: a product leaves half's range long before it loses precision, so widening the accumulator would only hide the overflow.
|
|
251
|
+
|
|
252
|
+
The widening protects the accumulation, not the answer, which is still stored as half.
|
|
253
|
+
A variance above 65504 therefore saturates even though nothing overflowed while it was being computed, and the standard deviation of the same array is fine because the square root brings it back into range:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
a = Cumo::HFloat[2000.0, -2000.0, 1000.0, -1000.0]
|
|
257
|
+
a.var #=> Infinity
|
|
258
|
+
a.stddev #=> 1826.0
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
That distinction is what makes half usable in a transformer's layer normalization, and getting it wrong is the first thing to go wrong there.
|
|
262
|
+
Squaring overflows at 256, since 256 squared is already past the top, and a residual stream with one outlier feature reaches thousands.
|
|
263
|
+
Writing the normalization out as `((x - mean) ** 2).mean` builds those squares as a half array and the answer is `Infinity`, while `x.var(axis: 1)` keeps them in its single-precision accumulator and answers 10208.0 against a single-precision 10209.
|
|
264
|
+
`var` saturates only if the variance itself is out of range, which is a far higher bar than any one deviation being over 256, but it is still a bar.
|
|
265
|
+
The quantity to check against 65504 is the largest variance a layer produces, not the largest activation in it.
|
|
266
|
+
A single outlier of size d among n values contributes only d squared over n to the variance, so a row 768 wide divides it by 768: an activation of 3000 squares to nine million but raises the variance of its row by about twelve thousand.
|
|
267
|
+
An activation that looks safe therefore says nothing about whether `var` overflows, in either direction.
|
|
268
|
+
|
|
269
|
+
A `dot` whose answer does not fit still saturates, since the result is stored back as half.
|
|
270
|
+
The accumulator itself is single precision, so scaling it down on the way out recovers the value:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
a = Cumo::HFloat.ones(1, 1024)
|
|
274
|
+
b = Cumo::HFloat.new(1024, 1).fill(100.0)
|
|
275
|
+
a.dot(b) #=> Infinity
|
|
276
|
+
a.gemm(b, alpha: 0.001) #=> 102.375, the true 102400 scaled down
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### What half is faster at
|
|
280
|
+
|
|
281
|
+
`gemm` reaches the tensor cores. Square matrices on an RTX 5070 Ti Laptop, median of three runs each:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
HFloat SFloat DFloat
|
|
285
|
+
1024x1024 0.043 ms 49.7 TF 0.161 ms 13.3 TF 5.370 ms 0.40 TF
|
|
286
|
+
2048x2048 0.362 ms 47.4 TF 1.360 ms 12.6 TF 43.46 ms 0.40 TF
|
|
287
|
+
4096x4096 3.335 ms 41.2 TF 10.40 ms 13.2 TF 328.1 ms 0.42 TF
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
An odd number of columns costs half far more than it costs the others, because a row then starts on a two-byte boundary and the vectorized path is gone.
|
|
291
|
+
It is the column count of either operand that matters, not the row count.
|
|
292
|
+
1024x1024 times 1024x1024, with one dimension made odd at a time:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
all even M odd K odd N odd
|
|
296
|
+
HFloat 51.8 TF 49.2 TF 23.3 TF 23.3 TF
|
|
297
|
+
SFloat 11.9 TF - 11.6 TF 10.8 TF
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The run-to-run spread on these is a few per cent and reaches fifteen at the top end, so the M column says the row count does not matter rather than that it costs 5 per cent.
|
|
301
|
+
|
|
302
|
+
The penalty is on the arithmetic, so it does not reach a matrix-vector product, which is bound by how fast the matrix can be read whatever its shape.
|
|
303
|
+
A 1x768 by 768x50257 gemv takes 0.211 ms with that odd 50257 and 0.205 ms with 50256, a difference inside the noise; the same 768x50257 matrix against 256 rows takes 0.833 ms and 0.481 ms, which is not.
|
|
304
|
+
Pad the inner dimensions of a real matrix product; leave a gemv alone.
|
|
305
|
+
|
|
306
|
+
`conv` is a different story, and worth reading before reaching for half in a network.
|
|
307
|
+
cuDNN chooses its algorithm from the ones that fit in a scratch buffer, and the half algorithms that use the tensor cores ask for more than the default 8MB ceiling allows.
|
|
308
|
+
Left at the default, a half convolution is no faster than a single-precision one.
|
|
309
|
+
N=32, C=K=64, 56x56, 3x3:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
HFloat SFloat
|
|
313
|
+
CUMO_CUDNN_MAX_WORKSPACE_SIZE unset 1.13 ms 1.03 ms
|
|
314
|
+
CUMO_CUDNN_MAX_WORKSPACE_SIZE=268435456 0.56 ms 0.68 ms
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Tensor cores also want the channel counts to be multiples of eight, which the first layer of a network never satisfies.
|
|
318
|
+
That layer is still faster in half, but for the other reason:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
C=3, K=64, 56x56, 3x3 0.085 ms 0.170 ms
|
|
322
|
+
C=K=64, 56x56, 1x1 0.037 ms 0.120 ms
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Neither of those reaches a tensor core; they move half the bytes.
|
|
326
|
+
|
|
327
|
+
#### Batch normalization takes single-precision parameters
|
|
328
|
+
|
|
329
|
+
cuDNN derives the descriptor for the batch norm parameters from `x`, and widens it to float when `x` is half.
|
|
330
|
+
`gamma`, `beta`, `running_mean`, `running_var`, `mean` and `inv_std` are therefore `Cumo::SFloat` where `x` is `Cumo::HFloat`, and `batch_norm_backward` answers `gx` in half with `ggamma` and `gbeta` in single:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
x = Cumo::HFloat.new(2, 4, 3, 3).seq
|
|
334
|
+
gamma = Cumo::SFloat.ones(4)
|
|
335
|
+
beta = Cumo::SFloat.zeros(4)
|
|
336
|
+
x.batch_norm(gamma, beta, axis: [0, 2, 3]) #=> Cumo::HFloat
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Passing half parameters raises `TypeError: gamma must be Cumo::SFloat, not Cumo::HFloat`.
|
|
340
|
+
Keeping the running statistics in single precision is what the arithmetic wants in any case: a momentum update is a long chain of small corrections, and eleven bits of mantissa lose them.
|
|
341
|
+
|
|
342
|
+
Gradients are the other place half runs out of room.
|
|
343
|
+
Values below `Cumo::HFloat::MIN` of 6.1e-05 fall into the subnormals and then to zero, which is what loss scaling in a training loop exists to prevent.
|
|
344
|
+
|
|
225
345
|
### Select a GPU device ID
|
|
226
346
|
|
|
227
347
|
Set the `CUDA_VISIBLE_DEVICES=id` environment variable, or
|
|
@@ -363,6 +483,19 @@ export CUMO_SHOW_WARNING=ON
|
|
|
363
483
|
export CUMO_SHOW_WARNING_ONCE=OFF
|
|
364
484
|
```
|
|
365
485
|
|
|
486
|
+
### Raise the cuDNN workspace ceiling
|
|
487
|
+
|
|
488
|
+
cuDNN picks a convolution algorithm by benchmarking the ones that fit in a scratch buffer, and the ceiling on that buffer is 8MB.
|
|
489
|
+
The fastest half precision algorithms, the ones that reach the tensor cores, ask for more than that and are left out of the search.
|
|
490
|
+
To let them in:
|
|
491
|
+
|
|
492
|
+
```
|
|
493
|
+
export CUMO_CUDNN_MAX_WORKSPACE_SIZE=67108864
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
The value is in bytes and only bounds the search; each convolution reserves what its chosen algorithm actually needs.
|
|
497
|
+
`Cumo::CUDA::CUDNN.max_workspace_size` reads back the value in force.
|
|
498
|
+
|
|
366
499
|
## Contributing
|
|
367
500
|
|
|
368
501
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sonots/cumo.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# Cumo shape sweep
|
|
6
|
+
#
|
|
7
|
+
# cumo_probe.rb runs every kernel family at one shape and reads each member
|
|
8
|
+
# against the fastest of its group. What that cannot see is a family whose
|
|
9
|
+
# speed depends on the shape it is handed: a reduction along a short row, an
|
|
10
|
+
# operand that is broadcast or strided, a copy that transposes. This sweep
|
|
11
|
+
# holds the bytes fixed, varies the shape and the layout, and reads every case
|
|
12
|
+
# against the same bytes moved down the best path there is for them: the flat
|
|
13
|
+
# reduction, the add of two contiguous arrays, the contiguous copy.
|
|
14
|
+
#
|
|
15
|
+
# GPU=1 ruby cumo_shape_probe.rb # the default sweep
|
|
16
|
+
# GPU=1 GROUP=reduce ruby cumo_shape_probe.rb # reduce, elementwise or copy
|
|
17
|
+
# GPU=1 ALL=1 ruby cumo_shape_probe.rb # every row, not just the flagged
|
|
18
|
+
# GPU=1 FLAG=3 ruby cumo_shape_probe.rb # how far off the reference is called out
|
|
19
|
+
# GPU=1 DTYPE=DFloat ELEMENTS=67108864 ruby cumo_shape_probe.rb
|
|
20
|
+
# GPU=1 JSON=before.json ruby cumo_shape_probe.rb # keep this run's rows ...
|
|
21
|
+
# GPU=1 COMPARE=before.json ruby cumo_shape_probe.rb # ... and read a later build against them
|
|
22
|
+
# ruby cumo_shape_probe.rb # the same sweep on Numo
|
|
23
|
+
#
|
|
24
|
+
# Every case queues LAUNCHES calls and synchronizes once, and the run opens
|
|
25
|
+
# with a few seconds of load. On a laptop the memory clock climbs in steps,
|
|
26
|
+
# and a best-of-N that synchronizes between calls can settle on a step below
|
|
27
|
+
# the top and read a third low without anything being wrong.
|
|
28
|
+
#
|
|
29
|
+
# The sweep runs in one process. The cases work in place or reduce, so the
|
|
30
|
+
# only results allocated are the reductions', a thousandth of their input.
|
|
31
|
+
#
|
|
32
|
+
# The default 4M elements of SFloat is 16 MB, inside the L2 of this class of
|
|
33
|
+
# GPU, which is where the activations of a model live. ELEMENTS=67108864 moves
|
|
34
|
+
# the sweep to DRAM, where a cost paid per element hides behind the bandwidth.
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
require 'json'
|
|
38
|
+
|
|
39
|
+
GPU = !%w[0 false].include?(ENV['GPU'].to_s.downcase) && !ENV['GPU'].to_s.empty?
|
|
40
|
+
|
|
41
|
+
if GPU
|
|
42
|
+
require 'cumo/narray'
|
|
43
|
+
XM = Cumo
|
|
44
|
+
else
|
|
45
|
+
require 'numo/narray'
|
|
46
|
+
XM = Numo
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
GROUP = ENV['GROUP']
|
|
50
|
+
FLAG = (ENV['FLAG'] || 2.0).to_f
|
|
51
|
+
SHOW_ALL = !ENV['ALL'].to_s.empty?
|
|
52
|
+
LAUNCHES = (ENV['LAUNCHES'] || 10).to_i
|
|
53
|
+
ROUNDS = (ENV['ROUNDS'] || 4).to_i
|
|
54
|
+
ELEMENTS = (ENV['ELEMENTS'] || (1 << 22)).to_i
|
|
55
|
+
DTYPE = XM.const_get(ENV['DTYPE'] || 'SFloat')
|
|
56
|
+
JSON_OUT = ENV['JSON']
|
|
57
|
+
COMPARE = ENV['COMPARE']
|
|
58
|
+
|
|
59
|
+
def sync
|
|
60
|
+
XM::CUDA::Runtime.cudaDeviceSynchronize if GPU
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def now
|
|
64
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def elmsz
|
|
68
|
+
DTYPE::ELEMENT_BYTE_SIZE
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def warm(seconds)
|
|
72
|
+
a = DTYPE.new(1 << 22).fill(0)
|
|
73
|
+
t = now
|
|
74
|
+
a.inplace + 1 while now - t < seconds
|
|
75
|
+
sync
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Seconds for one call, best of ROUNDS batches of LAUNCHES calls.
|
|
79
|
+
def time_one
|
|
80
|
+
2.times { yield }
|
|
81
|
+
sync
|
|
82
|
+
best = Float::INFINITY
|
|
83
|
+
ROUNDS.times do
|
|
84
|
+
sync
|
|
85
|
+
t = now
|
|
86
|
+
LAUNCHES.times { yield }
|
|
87
|
+
sync
|
|
88
|
+
best = [best, (now - t) / LAUNCHES].min
|
|
89
|
+
end
|
|
90
|
+
best
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Row = Struct.new(:group, :name, :shape, :us, :gbs, :ref, keyword_init: true) do
|
|
94
|
+
def key
|
|
95
|
+
"#{group}/#{name}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
ROWS = []
|
|
100
|
+
|
|
101
|
+
# bytes is what the case moves, and the reference row is the one this case is
|
|
102
|
+
# read against.
|
|
103
|
+
def probe(group, name, shape, bytes, ref: nil, &blk)
|
|
104
|
+
return if GROUP && !group.start_with?(GROUP)
|
|
105
|
+
|
|
106
|
+
s = time_one(&blk)
|
|
107
|
+
ROWS << Row.new(group: group, name: name, shape: shape.inspect, us: s * 1e6, gbs: bytes / s / 1e9, ref: ref || name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def rand_array(*shape)
|
|
111
|
+
a = DTYPE.new(*shape)
|
|
112
|
+
DTYPE.name.include?('Int') ? a.rand(100) : a.rand
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# --- reductions ---------------------------------------------------------------
|
|
116
|
+
#
|
|
117
|
+
# The same ELEMENTS reduced along the last axis, the first axis and a middle
|
|
118
|
+
# axis, over rows of every length, all read against the flat reduction of the
|
|
119
|
+
# same array. Each output of every case is a reduction over len elements.
|
|
120
|
+
def sweep_reduce
|
|
121
|
+
n = ELEMENTS
|
|
122
|
+
es = elmsz
|
|
123
|
+
a = rand_array(n)
|
|
124
|
+
b = rand_array(n)
|
|
125
|
+
methods = %i[sum max mean argmax mulsum].select { |m| a.respond_to?(m) }
|
|
126
|
+
methods.each do |m|
|
|
127
|
+
bytes = (m == :mulsum ? 2 : 1) * n * es
|
|
128
|
+
probe('reduce', "#{m} flat", [n], bytes) { m == :mulsum ? a.mulsum(b) : a.send(m) }
|
|
129
|
+
end
|
|
130
|
+
[4, 16, 64, 256, 1024, 4096].each do |len|
|
|
131
|
+
next if len > n
|
|
132
|
+
|
|
133
|
+
rows = n / len
|
|
134
|
+
r1 = 1 << (Math.log2(rows).to_i / 2)
|
|
135
|
+
r2 = rows / r1
|
|
136
|
+
last = a.reshape(rows, len)
|
|
137
|
+
last_b = b.reshape(rows, len)
|
|
138
|
+
first = a.reshape(len, rows)
|
|
139
|
+
first_b = b.reshape(len, rows)
|
|
140
|
+
mid = a.reshape(r1, len, r2)
|
|
141
|
+
mid_b = b.reshape(r1, len, r2)
|
|
142
|
+
methods.each do |m|
|
|
143
|
+
bytes = (m == :mulsum ? 2 : 1) * n * es
|
|
144
|
+
ref = "#{m} flat"
|
|
145
|
+
call = lambda do |x, y, axis|
|
|
146
|
+
m == :mulsum ? x.mulsum(y, axis: axis) : x.send(m, axis: axis)
|
|
147
|
+
end
|
|
148
|
+
probe('reduce', "#{m} len #{len} last", [rows, len], bytes, ref: ref) { call.call(last, last_b, 1) }
|
|
149
|
+
probe('reduce', "#{m} len #{len} first", [len, rows], bytes, ref: ref) { call.call(first, first_b, 0) }
|
|
150
|
+
probe('reduce', "#{m} len #{len} middle", [r1, len, r2], bytes, ref: ref) { call.call(mid, mid_b, 1) }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# --- elementwise --------------------------------------------------------------
|
|
156
|
+
#
|
|
157
|
+
# One [R, C] array added to in place, its operand contiguous, a scalar, a
|
|
158
|
+
# broadcast row or column, a slice, a stepped view or a transpose, plus the
|
|
159
|
+
# same broadcast through three and four dimensions. The reference is the add
|
|
160
|
+
# of two contiguous arrays, which the loop walks as one run.
|
|
161
|
+
def sweep_elementwise
|
|
162
|
+
n = ELEMENTS
|
|
163
|
+
es = elmsz
|
|
164
|
+
r = 1024
|
|
165
|
+
c = n / r
|
|
166
|
+
a = rand_array(r, c)
|
|
167
|
+
b = rand_array(r, c)
|
|
168
|
+
row = rand_array(c)
|
|
169
|
+
col = rand_array(r, 1)
|
|
170
|
+
probe('elementwise', 'a + b contiguous', [r, c], 3 * n * es) { a.inplace + b }
|
|
171
|
+
probe('elementwise', 'a * scalar', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace * 2 }
|
|
172
|
+
probe('elementwise', 'a + row broadcast', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace + row }
|
|
173
|
+
probe('elementwise', 'a - column broadcast', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.inplace - col }
|
|
174
|
+
a3 = a.reshape(16, r / 16, c)
|
|
175
|
+
b3 = rand_array(1, r / 16, 1)
|
|
176
|
+
probe('elementwise', 'a + broadcast 3d', [16, r / 16, c], 2 * n * es, ref: 'a + b contiguous') { a3.inplace + b3 }
|
|
177
|
+
a4 = a.reshape(4, 4, r / 16, c)
|
|
178
|
+
b4 = rand_array(1, 4, 1, c)
|
|
179
|
+
probe('elementwise', 'a + broadcast 4d', [4, 4, r / 16, c], 2 * n * es, ref: 'a + b contiguous') { a4.inplace + b4 }
|
|
180
|
+
half = 0...(c / 2)
|
|
181
|
+
probe('elementwise', 'a + b column slice', [r, c / 2], 3 * n * es / 2, ref: 'a + b contiguous') { a[true, half].inplace + b[true, half] }
|
|
182
|
+
every_other = (0..-1) % 2
|
|
183
|
+
probe('elementwise', 'a * scalar stepped rows', [r / 2, c], n * es, ref: 'a + b contiguous') { a[every_other, true].inplace * 2 }
|
|
184
|
+
probe('elementwise', 'a + b transposed views', [c, r], 3 * n * es, ref: 'a + b contiguous') { a.transpose.inplace + b.transpose }
|
|
185
|
+
probe('elementwise', 'a.gt(b) to Bit', [r, c], 2 * n * es, ref: 'a + b contiguous') { a.gt(b) }
|
|
186
|
+
probe('elementwise', 'a.gt(row) to Bit', [r, c], n * es, ref: 'a + b contiguous') { a.gt(row) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# --- copies -------------------------------------------------------------------
|
|
190
|
+
#
|
|
191
|
+
# A store from a view of every kind into contiguous memory, read against the
|
|
192
|
+
# contiguous copy of the same bytes.
|
|
193
|
+
def sweep_copy
|
|
194
|
+
n = ELEMENTS
|
|
195
|
+
es = elmsz
|
|
196
|
+
r = 1024
|
|
197
|
+
c = n / r
|
|
198
|
+
a = rand_array(r, c)
|
|
199
|
+
dst = DTYPE.new(r, c).fill(0)
|
|
200
|
+
dst_t = DTYPE.new(c, r).fill(0)
|
|
201
|
+
probe('copy', 'contiguous', [r, c], 2 * n * es) { dst.store(a) }
|
|
202
|
+
half = 0...(c / 2)
|
|
203
|
+
probe('copy', 'column slice', [r, c / 2], n * es, ref: 'contiguous') { dst[true, half].store(a[true, half]) }
|
|
204
|
+
every_other = (0..-1) % 2
|
|
205
|
+
probe('copy', 'stepped rows', [r / 2, c], n * es, ref: 'contiguous') { dst[every_other, true].store(a[every_other, true]) }
|
|
206
|
+
probe('copy', 'reversed rows', [r, c], 2 * n * es, ref: 'contiguous') { dst.store(a.reverse(0)) }
|
|
207
|
+
probe('copy', 'transposed', [c, r], 2 * n * es, ref: 'contiguous') { dst_t.store(a.transpose) }
|
|
208
|
+
idx = XM::Int32.new(r / 2).seq * 2
|
|
209
|
+
probe('copy', 'index-backed rows', [r / 2, c], n * es, ref: 'contiguous') { dst[0...(r / 2), true].store(a[idx, true]) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# --- run ----------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
version = GPU ? Cumo::NArray::VERSION : Numo::NArray::VERSION
|
|
215
|
+
puts "backend : #{XM} #{version}"
|
|
216
|
+
puts "dtype : #{DTYPE}, #{ELEMENTS} elements (#{(ELEMENTS * elmsz / 1e6).round(1)} MB), #{LAUNCHES} launches per sync, best of #{ROUNDS}"
|
|
217
|
+
warm(3)
|
|
218
|
+
sweep_reduce
|
|
219
|
+
sweep_elementwise
|
|
220
|
+
sweep_copy
|
|
221
|
+
|
|
222
|
+
refs = ROWS.to_h { |row| [row.key, row] }
|
|
223
|
+
flagged = []
|
|
224
|
+
puts
|
|
225
|
+
puts format(' %-11s %-26s %-18s %9s %8s %8s', 'group', 'case', 'shape', 'us', 'GB/s', 'vs ref')
|
|
226
|
+
ROWS.each do |row|
|
|
227
|
+
ref = refs["#{row.group}/#{row.ref}"]
|
|
228
|
+
ratio = ref ? ref.gbs / row.gbs : 1.0
|
|
229
|
+
slow = ratio > FLAG
|
|
230
|
+
flagged << row if slow
|
|
231
|
+
next unless slow || SHOW_ALL || row.ref == row.name
|
|
232
|
+
|
|
233
|
+
puts format(' %-11s %-26s %-18s %9.1f %8.1f %7.1fx%s', row.group, row.name, row.shape, row.us, row.gbs, ratio, slow ? ' <--' : '')
|
|
234
|
+
end
|
|
235
|
+
puts
|
|
236
|
+
if flagged.empty?
|
|
237
|
+
puts format(' nothing was more than %.1fx off its reference', FLAG)
|
|
238
|
+
else
|
|
239
|
+
puts format(' %d of %d cases more than %.1fx off their reference', flagged.size, ROWS.size, FLAG)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
if COMPARE
|
|
243
|
+
before = JSON.parse(File.read(COMPARE))
|
|
244
|
+
puts
|
|
245
|
+
puts " against #{File.basename(COMPARE)} (#{before['meta']['backend']} #{before['meta']['version']}, saved #{before['meta']['saved']})"
|
|
246
|
+
puts format(' %-11s %-26s %9s %9s %8s', 'group', 'case', 'before us', 'after us', 'speedup')
|
|
247
|
+
ROWS.each do |row|
|
|
248
|
+
was = before['rows'][row.key]
|
|
249
|
+
next unless was
|
|
250
|
+
|
|
251
|
+
speedup = was['us'] / row.us
|
|
252
|
+
next unless SHOW_ALL || speedup > 1.25 || speedup < 0.8
|
|
253
|
+
|
|
254
|
+
puts format(' %-11s %-26s %9.1f %9.1f %7.2fx', row.group, row.name, was['us'], row.us, speedup)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if JSON_OUT
|
|
259
|
+
out = { 'meta' => { 'backend' => XM.to_s, 'version' => version, 'dtype' => DTYPE.to_s, 'elements' => ELEMENTS,
|
|
260
|
+
'saved' => Time.now.strftime('%Y-%m-%d %H:%M') },
|
|
261
|
+
'rows' => ROWS.to_h { |row| [row.key, { 'shape' => row.shape, 'us' => row.us, 'gbs' => row.gbs }] } }
|
|
262
|
+
File.write(JSON_OUT, JSON.pretty_generate(out))
|
|
263
|
+
puts " wrote #{ROWS.size} rows to #{JSON_OUT}"
|
|
264
|
+
end
|
data/bench/cumo_sync_probe.rb
CHANGED
|
@@ -90,8 +90,8 @@ end
|
|
|
90
90
|
|
|
91
91
|
PROBES = []
|
|
92
92
|
|
|
93
|
-
def probe(group, name, &setup)
|
|
94
|
-
PROBES << { group: group, name: name, setup: setup }
|
|
93
|
+
def probe(group, name, counts_only: false, &setup)
|
|
94
|
+
PROBES << { group: group, name: name, setup: setup, counts_only: counts_only }
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# Reading data back is meant to synchronize. The question is whether it says so.
|
|
@@ -283,6 +283,47 @@ probe('compute', 'index aref a[idx, true]') do
|
|
|
283
283
|
end
|
|
284
284
|
|
|
285
285
|
# Host to device
|
|
286
|
+
# A block walks the array on the host and can queue work on it, so the walk has
|
|
287
|
+
# to wait between elements. It only has to wait when the block queued something,
|
|
288
|
+
# which is what the idle count says: nothing for a block that stays off the
|
|
289
|
+
# device, once per touched element for one that does not.
|
|
290
|
+
BLOCK_N = 32
|
|
291
|
+
|
|
292
|
+
probe('block', 'each (block off the device)', counts_only: true) do
|
|
293
|
+
a = Cumo::SFloat.new(BLOCK_N).seq
|
|
294
|
+
-> { a.each { |x| x } }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
probe('block', 'each (block writes)', counts_only: true) do
|
|
298
|
+
a = Cumo::SFloat.new(BLOCK_N).seq
|
|
299
|
+
-> { i = 0; a.each { |_x| a[i] = 1; i += 1 } }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
probe('block', 'each_with_index (block writes)', counts_only: true) do
|
|
303
|
+
a = Cumo::SFloat.new(BLOCK_N).seq
|
|
304
|
+
-> { a.each_with_index { |_x, i| a[i] = 1 } }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
probe('block', 'map (block writes)', counts_only: true) do
|
|
308
|
+
a = Cumo::SFloat.new(BLOCK_N).seq
|
|
309
|
+
-> { i = 0; a.map { |x| a[i] = 1; i += 1; x } }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
probe('block', 'map_with_index (block writes)', counts_only: true) do
|
|
313
|
+
a = Cumo::SFloat.new(BLOCK_N).seq
|
|
314
|
+
-> { a.map_with_index { |x, i| a[i] = 1; x } }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
probe('block', 'Bit each (block writes)', counts_only: true) do
|
|
318
|
+
a = Cumo::Bit.new(BLOCK_N).fill(1)
|
|
319
|
+
-> { i = 0; a.each { |_x| a[i] = 0; i += 1 } }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
probe('block', 'Bit each_with_index (block writes)', counts_only: true) do
|
|
323
|
+
a = Cumo::Bit.new(BLOCK_N).fill(1)
|
|
324
|
+
-> { a.each_with_index { |_x, i| a[i] = 0 } }
|
|
325
|
+
end
|
|
326
|
+
|
|
286
327
|
probe('h2d', 'from_binary') do
|
|
287
328
|
bin = Array.new(BIG_N / 4, 1.0).pack('f*')
|
|
288
329
|
-> { Cumo::SFloat.from_binary(bin, [BIG_N / 4]) }
|
|
@@ -347,8 +388,8 @@ puts
|
|
|
347
388
|
|
|
348
389
|
results = []
|
|
349
390
|
|
|
350
|
-
puts format(' %-
|
|
351
|
-
puts " #{'-' *
|
|
391
|
+
puts format(' %-34s %10s %6s %12s %-9s %s', 'probe', 'idle', 'waits', 'behind queue', 'verdict', 'warning')
|
|
392
|
+
puts " #{'-' * 99}"
|
|
352
393
|
|
|
353
394
|
current_group = nil
|
|
354
395
|
PROBES.each do |p|
|
|
@@ -363,7 +404,22 @@ PROBES.each do |p|
|
|
|
363
404
|
begin
|
|
364
405
|
call = p[:setup].call
|
|
365
406
|
if call.nil?
|
|
366
|
-
puts format(' %-
|
|
407
|
+
puts format(' %-34s %10s %6s %12s %-9s %s', p[:name], '-', '-', '-', 'skip', '(no such API)')
|
|
408
|
+
next
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# (1b) how many times it waits with nothing queued. With the warning left
|
|
412
|
+
# on every occurrence this is the wait count, not a yes or no.
|
|
413
|
+
sync
|
|
414
|
+
idle_text = capture_stdio { call.call }
|
|
415
|
+
idle_warns = idle_text.to_s.lines.grep(/warn|sync|Sync|WARN/i).size
|
|
416
|
+
|
|
417
|
+
if p[:counts_only]
|
|
418
|
+
# Timing these would measure the warning writes, not the call.
|
|
419
|
+
puts format(' %-34s %10s %6d %12s %-9s %s',
|
|
420
|
+
p[:name], '-', idle_warns, '-', 'waits', '-')
|
|
421
|
+
results << { name: p[:name], group: p[:group], sync: idle_warns.positive?,
|
|
422
|
+
warned: idle_warns.positive? }
|
|
367
423
|
next
|
|
368
424
|
end
|
|
369
425
|
|
|
@@ -412,12 +468,12 @@ PROBES.each do |p|
|
|
|
412
468
|
format('x%d %s', warns.size, warns.first.to_s.strip[0, 40])
|
|
413
469
|
end
|
|
414
470
|
|
|
415
|
-
puts format(' %-
|
|
416
|
-
p[:name], idle * 1e6, blocked * 1e3,
|
|
471
|
+
puts format(' %-34s %8.1f us %6d %10.2f ms %-9s %s',
|
|
472
|
+
p[:name], idle * 1e6, idle_warns, blocked * 1e3,
|
|
417
473
|
is_sync ? 'SYNC' : 'async', note)
|
|
418
474
|
rescue StandardError, NotImplementedError => e
|
|
419
475
|
msg = e.message.to_s.split("\n").first.to_s[0, 40]
|
|
420
|
-
puts format(' %-
|
|
476
|
+
puts format(' %-34s %10s %6s %12s %-9s %s', p[:name], '-', '-', '-', 'ERROR', "#{e.class}: #{msg}")
|
|
421
477
|
end
|
|
422
478
|
end
|
|
423
479
|
|
|
@@ -440,3 +496,10 @@ puts ' The first line is what this is for: those calls stall the pipeline and'
|
|
|
440
496
|
puts ' CUMO_SHOW_WARNING will not tell you. Either add the warning or, where'
|
|
441
497
|
puts ' the synchronization is avoidable, remove it.'
|
|
442
498
|
puts ' A SYNC in the meta group is waste with nothing to show for it.'
|
|
499
|
+
puts
|
|
500
|
+
puts ' The block group counts waits rather than timing them: a walk that hands'
|
|
501
|
+
puts ' every element to a Ruby block has to wait between elements, since the'
|
|
502
|
+
puts ' block can queue work on the array being walked. A block that stays off'
|
|
503
|
+
puts ' the device should cost none, and one that writes should cost about one'
|
|
504
|
+
puts ' per element. A count of one there means the walk waits once for the whole'
|
|
505
|
+
puts ' row, which is what made Bit#each and map answer differently from numo.'
|