cumo 0.14.0 → 0.14.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f09e6c09816ea16f1a27425e44170c7ea191b771f0a3b26b2b7298a1e53b6e6c
4
- data.tar.gz: f4739b41e92d5f5920ea694393f3ab71241e9b428a29416db71e91d5fcb0f13e
3
+ metadata.gz: '0872662f2f59cb4486df50f2f9b6dee5fc62fe938f6c47bafdb1ebdfc744c2f6'
4
+ data.tar.gz: 248f4ea1224ee0ff46069d16b424b1c90a98d81b33610506c58d45ffb9693c08
5
5
  SHA512:
6
- metadata.gz: d58332736efabf28caeec996882ceb99435885e41d017a40911c6a85346a4f03428e32cb7cae32e8dae9633e47f7497dc35075309ec32d61d607504ef5f61fb1
7
- data.tar.gz: d276cc4b4d19203ab34bfae56e96dd6e3220a780f07d3da8934e9c9241336fcb31c2040e013a7e461b47ec93461dcff21ca126597742c184e52221c0112bf196
6
+ metadata.gz: caf8d608d673afd9e71d3c421d2c096db503bb0054bf1ca846f33de738ccb2662ab53a6c715e6003f9a27b48d148d1e8f60f509076d53eee1b1bebf56c023e97
7
+ data.tar.gz: 78c2fd433b79741516f842b069adfa90c0452694a106554e99fd0ac5415684d6b82beb7f44dae63a09a745fc075bf21c1fb71276fa8ad691965f9ddd335e8d9a
data/CHANGELOG.md CHANGED
@@ -1,8 +1,21 @@
1
+ # 0.14.1 (2026/10/22)
2
+
3
+ Fixes:
4
+
5
+ * Fix a host loop such as `each` yielding the old values of an array after its block wrote the array through `store_binary`, an in-place `cumsum` or `cumprod`, or an in-place `map` or `map_with_index`, also one paused by an Enumerator (PR #631, PR #630)
6
+ * Fix an in-place `map_with_index` over a view that reaches an element more than once, such as `x[[0, 0, 0]]`, reading the element as it was before its own write. It now reads its own write, as `map` and numo do (PR #631)
7
+ * Fix the release gem recording the tag of the previous release as `Cumo::REVISION`, as the 0.14.0 gem does with `"v0.13.0-11-gafd8ead9"`. `rake release` now makes the tag before it builds the gem, so this gem gives `"v0.14.1"` (PR #626)
8
+
9
+ Changes:
10
+
11
+ * `+`, `-`, `*` and `/` between a complex array and a real array of the same precision, in either order, read the real array as it is rather than casting it to a complex array first. On 4M elements this makes SComplex with SFloat about 1.7 to 1.8 times faster, and DComplex divided by DFloat about 1.4 times. The answers are the same but for the sign of a NaN (PR #632)
12
+ * A host loop such as `each` whose block writes to the device copies the rest of the row, or of the array, again at once when copying the elements one at a time would cost more. With one write early in an `each_with_index` over 100,000 elements, the loop is about 90 times faster and about as fast as with no write. With a write at every 100th element it is about 12 times faster, and with a write at every other element about 16% slower (PR #631)
13
+
1
14
  # 0.14.0 (2026/10/18)
2
15
 
3
16
  Breaking changes:
4
17
 
5
- * `cumo.h` no longer defines `CUMO_VERSION` or `CUMO_VERSION_CODE`. `Cumo::VERSION` and `Cumo::NArray::VERSION` are defined in Ruby, and the new `Cumo::REVISION` tells a release gem from one built from a checkout: it gives `"v0.14.0"` for this release and something like `"v0.14.0-3-g1a2b3c4"` for a gem built at a later commit (PR #617)
18
+ * `cumo.h` no longer defines `CUMO_VERSION` or `CUMO_VERSION_CODE`. `Cumo::VERSION` and `Cumo::NArray::VERSION` are defined in Ruby. The new `Cumo::REVISION` tells a release gem from one built from a checkout. A release gives its tag, and a gem built at a later commit gives something like `"v0.14.0-3-g1a2b3c4d"`. The 0.14.0 gem was built before its tag was made, so it gives `"v0.13.0-11-gafd8ead9"`. `rake release` makes the tag before the gem from the next release on (PR #617, PR #626)
6
19
 
7
20
  Fixes:
8
21
 
data/Rakefile CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "rubygems/package"
6
+
7
+ Rake::Task["release"].clear
8
+ desc "Tag the version, build the gem at the tag, and push the branch, the tag and the gem"
9
+ task :release, [:remote] do |_, args|
10
+ git = ->(*argv) { IO.popen(["git", *argv], err: File::NULL, &:read).strip }
11
+ version = File.read(File.join(__dir__, "lib/cumo/version.rb"))[/VERSION = "([^"]+)"/, 1]
12
+ tag = "v#{version}"
13
+ gem_file = "pkg/cumo-#{version}.gem"
14
+ remote = args[:remote] || "origin"
15
+ raise "There are files that need to be committed first." unless system("git", "diff", "--quiet") && system("git", "diff-index", "--quiet", "--cached", "HEAD")
16
+
17
+ tagged = git.call("rev-parse", "-q", "--verify", "#{tag}^{commit}")
18
+ raise "#{tag} is on #{tagged[0, 8]}, not on HEAD." unless tagged.empty? || tagged == git.call("rev-parse", "HEAD")
19
+
20
+ sh "git", "tag", "-m", "Version #{version}", tag if tagged.empty?
21
+ begin
22
+ mkdir_p "pkg"
23
+ sh "gem", "build", "cumo.gemspec", "--output", gem_file
24
+ revision = Gem::Package.new(gem_file).spec.metadata["cumo_revision"]
25
+ raise "#{gem_file} records #{revision.inspect} as cumo_revision, not #{tag.inspect}." unless revision == tag
26
+ rescue StandardError
27
+ sh "git", "tag", "-d", tag if tagged.empty?
28
+ raise
29
+ end
30
+ sh "git", "push", remote, "HEAD"
31
+ sh "git", "push", remote, "refs/tags/#{tag}"
32
+ sh "gem", "push", gem_file unless ENV["gem_push"] == "no"
33
+ end
5
34
 
6
35
  Rake::TestTask.new(:test) do |t|
7
36
  t.libs << "test"
@@ -33,10 +33,11 @@ extern size_t cumo_cuda_sync_epoch;
33
33
  // and reads again once the count moves.
34
34
  extern size_t cumo_cuda_launch_epoch;
35
35
 
36
- static inline void
36
+ // Answers the count before the write.
37
+ static inline size_t
37
38
  cumo_cuda_runtime_note_device_write(void)
38
39
  {
39
- cumo_atomic_size_fetch_add(&cumo_cuda_launch_epoch, 1);
40
+ return cumo_atomic_size_fetch_add(&cumo_cuda_launch_epoch, 1);
40
41
  }
41
42
 
42
43
  // A pinned host buffer for reading device memory back: a copy into pinned
@@ -104,6 +104,13 @@ typedef struct {
104
104
  // it will read next again if device memory was written since it was staged.
105
105
  void cumo_na_ndloop_refresh_next(cumo_na_loop_t *lp, const void *next, size_t bytes);
106
106
 
107
+ // For a function that yields to Ruby and writes an array that was there
108
+ // before it, which cumo_na_ndloop_writes_existing answers: counts the element
109
+ // it just wrote, so that a loop it yields to before it ends reads it. A staged
110
+ // copy of its own has to hold the element already.
111
+ int cumo_na_ndloop_writes_existing(cumo_na_loop_t *lp);
112
+ void cumo_na_ndloop_wrote(cumo_na_loop_t *lp);
113
+
107
114
  // spec of user function
108
115
  typedef struct {
109
116
  cumo_na_iter_func_t func; // user function
@@ -10,6 +10,13 @@
10
10
 
11
11
  <% unless type_name == 'robject' %>
12
12
  void <%="cumo_#{c_iter}_kernel_launch"%>(cumo_na_iarray_t* a1, cumo_na_iarray_t* a2, cumo_na_iarray_t* a3, cumo_na_indexer_t* indexer, int* divzero);
13
+ //<% if is_complex && %w[add sub mul div].include?(name) %>
14
+ void <%="cumo_#{c_iter}_r_kernel_launch"%>(cumo_na_iarray_t* a1, cumo_na_iarray_t* a2, cumo_na_iarray_t* a3, cumo_na_indexer_t* indexer, int real_is_left);
15
+ //<% end %>
16
+ //<% complex_of = {'sfloat' => ['scomplex', 'cumo_cSComplex'], 'dfloat' => ['dcomplex', 'cumo_cDComplex']}[type_name] if %w[add sub mul div].include?(name) %>
17
+ //<% if complex_of %>
18
+ VALUE <%="cumo_#{complex_of[0]}_#{name}_real_left"%>(VALUE real, VALUE complex);
19
+ //<% end %>
13
20
  //<% if %w[add sub mul div mod bit_and bit_or bit_xor left_shift right_shift copysign].include?(name) %>
14
21
  void <%="cumo_#{c_iter}_s_kernel_launch"%>(cumo_na_iarray_t* a1, dtype sv, cumo_na_iarray_t* a3, cumo_na_indexer_t* indexer, int* divzero, int scalar_is_left);
15
22
  //<% end %>
@@ -171,6 +178,40 @@ static void
171
178
  //<% end %>
172
179
  #undef check_intdivzero
173
180
 
181
+ //<% if is_complex && %w[add sub mul div].include?(name) %>
182
+ // The operands stay in the order they were written in, so that a shape
183
+ // mismatch names them as it always did.
184
+ static void
185
+ <%=c_iter%>_r(cumo_na_loop_t *const lp)
186
+ {
187
+ int real_is_left = *(int*)(lp->opt_ptr);
188
+ cumo_na_iarray_t a1 = cumo_na_make_iarray(&lp->args[real_is_left ? 1 : 0]);
189
+ cumo_na_iarray_t a2 = cumo_na_make_iarray(&lp->args[real_is_left ? 0 : 1]);
190
+ cumo_na_iarray_t a3 = cumo_na_make_iarray(&lp->args[2]);
191
+ cumo_na_indexer_t indexer = cumo_na_make_indexer(&lp->args[0]);
192
+
193
+ <%="cumo_#{c_iter}_r_kernel_launch"%>(&a1,&a2,&a3,&indexer,real_is_left);
194
+ }
195
+
196
+ // A real operand of this precision is read as it is rather than cast to a
197
+ // complex array first.
198
+ static VALUE
199
+ <%=c_func%>_real(VALUE lhs, VALUE rhs, int real_is_left)
200
+ {
201
+ cumo_ndfunc_arg_in_t ain[2] = {{real_is_left ? cRT : cT,0},{real_is_left ? cT : cRT,0}};
202
+ cumo_ndfunc_arg_out_t aout[1] = {{cT,0}};
203
+ cumo_ndfunc_t ndf = { <%=c_iter%>_r, CUMO_STRIDE_LOOP|CUMO_NDF_INDEXER_LOOP|CUMO_NDF_ANY_ORDER, 2, 1, ain, aout };
204
+
205
+ return cumo_na_ndloop3(&ndf, &real_is_left, 2, lhs, rhs);
206
+ }
207
+
208
+ VALUE
209
+ <%="cumo_#{type_name}_#{name}_real_left"%>(VALUE real, VALUE self)
210
+ {
211
+ return <%=c_func%>_real(real, self, 1);
212
+ }
213
+ //<% end %>
214
+
174
215
  static VALUE
175
216
  <%=c_func%>_self(VALUE self, VALUE other)
176
217
  {
@@ -202,6 +243,11 @@ static VALUE
202
243
  return cumo_na_ndloop3(&ndf_s, &y, 1, self);
203
244
  }
204
245
  //<% end %>
246
+ //<% if is_complex && %w[add sub mul div].include?(name) %>
247
+ if (rb_obj_class(other) == cRT) {
248
+ return <%=c_func%>_real(self, other, 0);
249
+ }
250
+ //<% end %>
205
251
  cumo_ndfunc_arg_in_t ain[2] = {{cT,0},{cT,0}};
206
252
  cumo_ndfunc_arg_out_t aout[1] = {{cT,0}};
207
253
  <% if type_name == 'robject' %>
@@ -230,6 +276,10 @@ static VALUE
230
276
  klass = cumo_na_upcast(rb_obj_class(self),rb_obj_class(other));
231
277
  if (klass==cT) {
232
278
  return <%=c_func%>_self(self, other);
279
+ //<% if complex_of %>
280
+ } else if (rb_obj_class(other) == <%=complex_of[1]%> && rb_obj_class(self) == cT) {
281
+ return <%="cumo_#{complex_of[0]}_#{name}_real_left"%>(self, other);
282
+ //<% end %>
233
283
  } else {
234
284
  v = rb_funcall(klass, cumo_id_cast, 1, self);
235
285
  return rb_funcall(v, <%=cumo_id_op%>, 1, other);
@@ -214,5 +214,29 @@ void <%="cumo_#{c_iter}_kernel_launch"%>(cumo_na_iarray_t* a1, cumo_na_iarray_t*
214
214
  cumo_cuda_runtime_check_kernel_launch();
215
215
  }
216
216
  //<% end %>
217
+
218
+ //<% if is_complex && %w[add sub mul div].include?(name) %>
219
+ // A real operand of the same precision is made complex as it is read, which is
220
+ // what casting it first did, without the array the cast wrote.
221
+ <% indexer_dims(true).each do |idim| %>
222
+ __global__ void <%="cumo_#{c_iter}_r_kernel_dim#{idim}"%>(cumo_na_iarray_t a1, cumo_na_iarray_t a2, cumo_na_iarray_t a3, cumo_na_indexer_t indexer, int real_is_left)
223
+ {
224
+ for (uint64_t i = blockIdx.x * blockDim.x + threadIdx.x; i < indexer.total_size; i += blockDim.x * gridDim.x) {
225
+ cumo_na_indexer_set_dim<%=idim%>(&indexer, i);
226
+ dtype x = *(dtype*)(cumo_na_iarray_at_dim<%=idim%>(&a1, &indexer));
227
+ dtype y = m_from_real(*(rtype*)(cumo_na_iarray_at_dim<%=idim%>(&a2, &indexer)));
228
+ *(dtype*)(cumo_na_iarray_at_dim<%=idim%>(&a3, &indexer)) = real_is_left ? m_<%=name%>(y, x) : m_<%=name%>(x, y);
229
+ }
230
+ }
231
+ <% end %>
232
+
233
+ void <%="cumo_#{c_iter}_r_kernel_launch"%>(cumo_na_iarray_t* a1, cumo_na_iarray_t* a2, cumo_na_iarray_t* a3, cumo_na_indexer_t* indexer, int real_is_left)
234
+ {
235
+ size_t grid_dim = cumo_get_grid_dim(indexer->total_size);
236
+ size_t block_dim = cumo_get_block_dim(indexer->total_size);
237
+ <%= indexer_switch("cumo_#{c_iter}_r_kernel", "*a1,*a2,*a3,*indexer,real_is_left", narrow: %w[a1 a2 a3]) %>
238
+ cumo_cuda_runtime_check_kernel_launch();
239
+ }
240
+ //<% end %>
217
241
  #undef cumo_check_intdivzero
218
242
  <% end %>
@@ -16,6 +16,12 @@ static void
16
16
  size_t *c;
17
17
  int nd, md;
18
18
 
19
+ // In place, a view that visits an element twice reads what it wrote the
20
+ // first time, so the copy the elements are read from takes the write too,
21
+ // after the element read next was copied again.
22
+ int own = lp->args[0].value == lp->args[1].value;
23
+ int counts = cumo_na_ndloop_writes_existing(lp);
24
+
19
25
  c = (size_t*)(lp->opt_ptr);
20
26
  cumo_na_with_index_dims(lp->ndim, &nd, &md);
21
27
  a = ALLOCA_N(VALUE,md);
@@ -30,40 +36,52 @@ static void
30
36
  if (idx1) {
31
37
  if (idx2) {
32
38
  for (; i--;) {
39
+ char *q = p1 + *idx1;
33
40
  CUMO_GET_DATA_INDEX(p1,idx1,dtype,x);
34
41
  x = yield_map_with_index(x,c,a,nd,md);
35
42
  if (i) cumo_na_ndloop_refresh_next(lp, p1 + *idx1, sizeof(dtype));
36
43
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
44
+ if (own) *(dtype*)q = x;
37
45
  CUMO_SET_DATA_INDEX(p2,idx2,dtype,x);
46
+ if (counts) cumo_na_ndloop_wrote(lp);
38
47
  c[nd]++;
39
48
  }
40
49
  } else {
41
50
  for (; i--;) {
51
+ char *q = p1 + *idx1;
42
52
  CUMO_GET_DATA_INDEX(p1,idx1,dtype,x);
43
53
  x = yield_map_with_index(x,c,a,nd,md);
44
54
  if (i) cumo_na_ndloop_refresh_next(lp, p1 + *idx1, sizeof(dtype));
45
55
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
56
+ if (own) *(dtype*)q = x;
46
57
  CUMO_SET_DATA_STRIDE(p2,s2,dtype,x);
58
+ if (counts) cumo_na_ndloop_wrote(lp);
47
59
  c[nd]++;
48
60
  }
49
61
  }
50
62
  } else {
51
63
  if (idx2) {
52
64
  for (; i--;) {
65
+ char *q = p1;
53
66
  CUMO_GET_DATA_STRIDE(p1,s1,dtype,x);
54
67
  x = yield_map_with_index(x,c,a,nd,md);
55
68
  if (i) cumo_na_ndloop_refresh_next(lp, p1, sizeof(dtype));
56
69
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
70
+ if (own) *(dtype*)q = x;
57
71
  CUMO_SET_DATA_INDEX(p2,idx2,dtype,x);
72
+ if (counts) cumo_na_ndloop_wrote(lp);
58
73
  c[nd]++;
59
74
  }
60
75
  } else {
61
76
  for (; i--;) {
77
+ char *q = p1;
62
78
  CUMO_GET_DATA_STRIDE(p1,s1,dtype,x);
63
79
  x = yield_map_with_index(x,c,a,nd,md);
64
80
  if (i) cumo_na_ndloop_refresh_next(lp, p1, sizeof(dtype));
65
81
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
82
+ if (own) *(dtype*)q = x;
66
83
  CUMO_SET_DATA_STRIDE(p2,s2,dtype,x);
84
+ if (counts) cumo_na_ndloop_wrote(lp);
67
85
  c[nd]++;
68
86
  }
69
87
  }
@@ -19,6 +19,8 @@ static void
19
19
  size_t i;
20
20
  dtype x;
21
21
  <% if name == 'map' %>
22
+ // RObject data is host memory, which no loop stages.
23
+ int counts = <%= type_name == 'robject' ? '0' : 'cumo_na_ndloop_writes_existing(lp)' %>;
22
24
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
23
25
  <% else %>
24
26
  CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>");
@@ -30,6 +32,7 @@ static void
30
32
  x = m_<%=name%>(x);
31
33
  <% if name == 'map' %>if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }<% end %>
32
34
  CUMO_SET_DATA_INDEX(p2,idx2,dtype,x);
35
+ <% if name == 'map' %>if (counts) cumo_na_ndloop_wrote(lp);<% end %>
33
36
  }
34
37
  } else {
35
38
  for (i=0; i<n; i++) {
@@ -37,6 +40,7 @@ static void
37
40
  x = m_<%=name%>(x);
38
41
  <% if name == 'map' %>if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }<% end %>
39
42
  CUMO_SET_DATA_STRIDE(p2,s2,dtype,x);
43
+ <% if name == 'map' %>if (counts) cumo_na_ndloop_wrote(lp);<% end %>
40
44
  }
41
45
  }
42
46
  } else {
@@ -46,6 +50,7 @@ static void
46
50
  x = m_<%=name%>(x);
47
51
  <% if name == 'map' %>if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }<% end %>
48
52
  CUMO_SET_DATA_INDEX(p2,idx2,dtype,x);
53
+ <% if name == 'map' %>if (counts) cumo_na_ndloop_wrote(lp);<% end %>
49
54
  }
50
55
  } else {
51
56
  //<% if need_align %>
@@ -58,6 +63,7 @@ static void
58
63
  x = m_<%=name%>(((dtype*)p1)[i]);
59
64
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
60
65
  ((dtype*)p2)[i] = x;
66
+ if (counts) cumo_na_ndloop_wrote(lp);
61
67
  <% else %>
62
68
  ((dtype*)p2)[i] = m_<%=name%>(((dtype*)p1)[i]);
63
69
  <% end %>
@@ -72,6 +78,7 @@ static void
72
78
  x = m_<%=name%>(*(dtype*)p1);
73
79
  if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }
74
80
  *(dtype*)p2 = x;
81
+ if (counts) cumo_na_ndloop_wrote(lp);
75
82
  <% else %>
76
83
  *(dtype*)p2 = m_<%=name%>(*(dtype*)p1);
77
84
  <% end %>
@@ -87,6 +94,7 @@ static void
87
94
  x = m_<%=name%>(x);
88
95
  <% if name == 'map' %>if (cumo_cuda_runtime_sync_if_busy()) { CUMO_SHOW_SYNCHRONIZE_FIXME_WARNING_ONCE("<%=name%>", "<%=type_name%>"); }<% end %>
89
96
  CUMO_SET_DATA_STRIDE(p2,s2,dtype,x);
97
+ <% if name == 'map' %>if (counts) cumo_na_ndloop_wrote(lp);<% end %>
90
98
  }
91
99
  //<% end %>
92
100
  }
@@ -1997,6 +1997,7 @@ cumo_na_store_binary(int argc, VALUE *argv, VALUE self)
1997
1997
  CUMO_SHOW_SYNCHRONIZE_WARNING_ONCE("cumo_na_store_binary", "any");
1998
1998
  cumo_cuda_runtime_device_synchronize();
1999
1999
  memcpy(ptr+cumo_na_get_offset(self), RSTRING_PTR(vstr)+offset, byte_size);
2000
+ cumo_cuda_runtime_note_device_write();
2000
2001
  }
2001
2002
 
2002
2003
  return SIZET2NUM(byte_size);
@@ -79,8 +79,17 @@ typedef struct CUMO_NA_HOST_STAGE {
79
79
  cumo_cuda_stage_t stage;
80
80
  size_t epoch; // the launch count when the whole range was copied
81
81
  size_t row_epoch; // the launch count when the current row was copied
82
+ size_t seen_epoch; // the launch count when the loop last looked
83
+ size_t row_cost; // what copying a row costs, see CUMO_NA_STAGE_COPY_COST
84
+ size_t range_cost; // what copying the whole range costs
85
+ size_t row_debt; // spent on single elements since the row was copied
86
+ size_t range_debt; // spent on rows and elements since the range was copied
82
87
  } cumo_na_host_stage_t;
83
88
 
89
+ // A copy into pinned memory costs a wait before its bytes, about as long as
90
+ // moving this many bytes takes, so a copy of n bytes costs this plus n.
91
+ #define CUMO_NA_STAGE_COPY_COST (64u << 10)
92
+
84
93
  #define LARG(lp,iarg) ((lp)->user.args[iarg])
85
94
  #define LITER(lp,idim,iarg) ((lp)->xargs[iarg].iter[idim])
86
95
  #define LITER_SRC(lp,idim) ((lp)->src_iter[idim])
@@ -476,6 +485,22 @@ ndloop_alloc(cumo_na_md_loop_t *lp, cumo_ndfunc_t *nf, VALUE args,
476
485
 
477
486
  static void ndloop_unstage_host_reads(cumo_na_md_loop_t *lp);
478
487
 
488
+ // Whether the loop wrote an array that was there before it: an input it
489
+ // overwrites, or an output it took over from an input. A new output is
490
+ // nothing anyone has staged.
491
+ static int
492
+ ndloop_writes_existing(cumo_na_md_loop_t *lp)
493
+ {
494
+ int j;
495
+
496
+ for (j=0; j<lp->narg; j++) {
497
+ if (j < lp->nin ? (lp->xargs[j].flag & CUMO_NDL_WRITE) : lp->xargs[j].flag == CUMO_NDL_READ_WRITE) {
498
+ return 1;
499
+ }
500
+ }
501
+ return 0;
502
+ }
503
+
479
504
  static VALUE
480
505
  ndloop_release(VALUE vlp)
481
506
  {
@@ -483,6 +508,13 @@ ndloop_release(VALUE vlp)
483
508
  VALUE v;
484
509
  cumo_na_md_loop_t *lp = (cumo_na_md_loop_t*)(vlp);
485
510
 
511
+ // A host loop, and a library call such as the scan of cumsum, writes
512
+ // device memory without a launch to count it, and a host loop reading a
513
+ // staged copy of that memory reads it again only once the count moves.
514
+ // Counted here so that a loop left by a raise counts too.
515
+ if (ndloop_writes_existing(lp)) {
516
+ cumo_cuda_runtime_note_device_write();
517
+ }
486
518
  ndloop_unstage_host_reads(lp);
487
519
  for (j=0; j < lp->narg; j++) {
488
520
  v = LARG(lp,j).value;
@@ -821,6 +853,7 @@ ndloop_set_output_narray(cumo_ndfunc_t *nf, cumo_na_md_loop_t *lp, int k,
821
853
  }
822
854
 
823
855
  j = lp->nin + k;
856
+ lp->xargs[j].flag = flag;
824
857
  LARG(lp,j).ndim = nd = nf->aout[k].dim;
825
858
  ndloop_set_stepidx(lp, j, v, dim_map, flag);
826
859
  if (nd > 0) {
@@ -1900,6 +1933,8 @@ static void
1900
1933
  ndloop_stage_row(cumo_na_md_loop_t *lp)
1901
1934
  {
1902
1935
  cumo_na_host_stage_t *hs = lp->hs;
1936
+ // Read before the copy: a write counted while it runs leaves it behind.
1937
+ size_t now = cumo_cuda_launch_epoch_now();
1903
1938
  int j;
1904
1939
 
1905
1940
  for (j=0; j<hs->narg; j++) {
@@ -1910,7 +1945,38 @@ ndloop_stage_row(cumo_na_md_loop_t *lp)
1910
1945
  ndloop_stage_bytes(lp, j, lo, hi, &min, &len);
1911
1946
  ndloop_stage_dma(lp, j, min, len);
1912
1947
  }
1913
- hs->row_epoch = cumo_cuda_launch_epoch_now();
1948
+ hs->row_epoch = now;
1949
+ hs->row_debt = 0;
1950
+ }
1951
+
1952
+ // Copies the whole range of every staged input again.
1953
+ static void
1954
+ ndloop_stage_range(cumo_na_md_loop_t *lp)
1955
+ {
1956
+ cumo_na_host_stage_t *hs = lp->hs;
1957
+ size_t now = cumo_cuda_launch_epoch_now();
1958
+ int j;
1959
+
1960
+ for (j=0; j<hs->narg; j++) {
1961
+ if (hs->ptr[j] == NULL) continue;
1962
+ ndloop_stage_dma(lp, j, hs->min[j], hs->len[j]);
1963
+ }
1964
+ hs->epoch = hs->row_epoch = now;
1965
+ hs->row_debt = hs->range_debt = 0;
1966
+ }
1967
+
1968
+ // The bytes a row of argument j spans, wherever the row starts. Bit rows
1969
+ // cover one word more than their bits when they start mid-word.
1970
+ static size_t
1971
+ ndloop_stage_row_len(cumo_na_md_loop_t *lp, int j)
1972
+ {
1973
+ ssize_t lo, hi;
1974
+
1975
+ ndloop_stage_extent(lp, j, lp->ndim, lp->hs->ndim, 0, &lo, &hi);
1976
+ if (rb_obj_is_kind_of(LARG(lp,j).value, cumo_cBit)) {
1977
+ return ((size_t)(hi - lo) / CUMO_NB + 2) * sizeof(CUMO_BIT_DIGIT);
1978
+ }
1979
+ return (size_t)(hi - lo) + LARG(lp,j).elmsz;
1914
1980
  }
1915
1981
 
1916
1982
  // Reads the inputs of a host loop into pinned memory and points the
@@ -1974,16 +2040,9 @@ ndloop_stage_host_reads(cumo_na_md_loop_t *lp)
1974
2040
  hs->rows_only = 1;
1975
2041
  total = 0;
1976
2042
  for (j=0; j<narg; j++) {
1977
- ssize_t lo, hi;
1978
2043
  size_t len;
1979
2044
  if (hs->ptr[j] == NULL) continue;
1980
- ndloop_stage_extent(lp, j, lp->ndim, nd, 0, &lo, &hi);
1981
- if (rb_obj_is_kind_of(LARG(lp,j).value, cumo_cBit)) {
1982
- // Which words a row covers depends on where it starts.
1983
- len = ((size_t)(hi - lo) / CUMO_NB + 2) * sizeof(CUMO_BIT_DIGIT);
1984
- } else {
1985
- len = (size_t)(hi - lo) + LARG(lp,j).elmsz;
1986
- }
2045
+ len = ndloop_stage_row_len(lp, j);
1987
2046
  if (len > CUMO_CUDA_STAGE_CACHE_MAX) {
1988
2047
  hs->ptr[j] = NULL;
1989
2048
  hs->direct++;
@@ -1997,59 +2056,121 @@ ndloop_stage_host_reads(cumo_na_md_loop_t *lp)
1997
2056
  if (hs->direct) cumo_cuda_runtime_device_synchronize();
1998
2057
  if (rows == 0) return;
1999
2058
  cumo_cuda_runtime_stage_alloc(&hs->stage, total);
2059
+ hs->row_cost = CUMO_NA_STAGE_COPY_COST + total;
2060
+ hs->seen_epoch = cumo_cuda_launch_epoch_now();
2000
2061
  return;
2001
2062
  }
2002
2063
  if (hs->direct) cumo_cuda_runtime_device_synchronize();
2003
2064
  if (total == 0) return;
2004
2065
  cumo_cuda_runtime_stage_alloc(&hs->stage, total);
2066
+ hs->range_cost = CUMO_NA_STAGE_COPY_COST + total;
2067
+ hs->row_cost = CUMO_NA_STAGE_COPY_COST;
2005
2068
  total = 0;
2006
2069
  for (j=0; j<narg; j++) {
2007
2070
  if (hs->ptr[j] == NULL) continue;
2008
2071
  hs->off[j] = total;
2009
2072
  LARG(lp,j).ptr = hs->stage.ptr + total - hs->min[j];
2010
- ndloop_stage_dma(lp, j, hs->min[j], hs->len[j]);
2073
+ hs->row_cost += ndloop_stage_row_len(lp, j);
2011
2074
  total += (hs->len[j] + 7) & ~(size_t)7;
2012
2075
  }
2013
- hs->epoch = hs->row_epoch = cumo_cuda_launch_epoch_now();
2076
+ ndloop_stage_range(lp);
2077
+ hs->seen_epoch = hs->epoch;
2014
2078
  }
2015
2079
 
2016
2080
  // Before each row: a row that is only copied as reached, or one that
2017
- // device memory was written since the copy.
2081
+ // device memory was written since the copy. Once the rows copied since a
2082
+ // write have cost as much as the whole range, and nothing was written while
2083
+ // the block ran last, the whole range is copied again instead.
2018
2084
  static inline void
2019
2085
  ndloop_stage_before_row(cumo_na_md_loop_t *lp)
2020
2086
  {
2021
2087
  cumo_na_host_stage_t *hs = lp->hs;
2088
+ size_t now;
2089
+ int quiet;
2090
+
2022
2091
  if (hs == NULL) return;
2023
2092
  if (hs->direct) cumo_cuda_runtime_sync_if_busy();
2024
- if (hs->stage.ptr && (hs->rows_only || hs->epoch != cumo_cuda_launch_epoch_now())) {
2093
+ if (hs->stage.ptr == NULL) return;
2094
+ now = cumo_cuda_launch_epoch_now();
2095
+ quiet = hs->seen_epoch == now;
2096
+ hs->seen_epoch = now;
2097
+ if (hs->rows_only) {
2025
2098
  ndloop_stage_row(lp);
2099
+ } else if (hs->epoch != now) {
2100
+ if (quiet && hs->range_debt >= hs->range_cost) {
2101
+ ndloop_stage_range(lp);
2102
+ } else {
2103
+ ndloop_stage_row(lp);
2104
+ hs->range_debt += hs->row_cost;
2105
+ }
2026
2106
  }
2027
2107
  }
2028
2108
 
2109
+ static inline cumo_na_md_loop_t *
2110
+ ndloop_of_user(cumo_na_loop_t *user)
2111
+ {
2112
+ return (cumo_na_md_loop_t*)((char*)user - offsetof(cumo_na_md_loop_t, user));
2113
+ }
2114
+
2029
2115
  // After a yield: the block may have written the element the user function
2030
2116
  // reads next, which is copied again on its own so that a block writing at
2031
2117
  // every element costs an element, not a row, each time.
2032
2118
  void
2033
2119
  cumo_na_ndloop_refresh_next(cumo_na_loop_t *user, const void *next, size_t bytes)
2034
2120
  {
2035
- cumo_na_md_loop_t *lp = (cumo_na_md_loop_t*)((char*)user - offsetof(cumo_na_md_loop_t, user));
2121
+ cumo_na_md_loop_t *lp = ndloop_of_user(user);
2036
2122
  cumo_na_host_stage_t *hs = lp->hs;
2037
- int j;
2123
+ size_t now;
2124
+ int quiet, j;
2038
2125
 
2039
2126
  if (hs == NULL) return;
2040
2127
  if (hs->direct) cumo_cuda_runtime_sync_if_busy();
2041
- if (hs->stage.ptr == NULL || hs->row_epoch == cumo_cuda_launch_epoch_now()) return;
2128
+ if (hs->stage.ptr == NULL) return;
2129
+ now = cumo_cuda_launch_epoch_now();
2130
+ quiet = hs->seen_epoch == now;
2131
+ hs->seen_epoch = now;
2132
+ if (hs->row_epoch == now) return;
2133
+ // Elements copied one at a time since a write have cost as much as the
2134
+ // row, and the block did not write this time: the row is copied again at
2135
+ // once. A block that writes at every element keeps to one.
2136
+ if (quiet && hs->row_debt >= hs->row_cost) {
2137
+ ndloop_stage_row(lp);
2138
+ hs->range_debt += hs->row_cost;
2139
+ return;
2140
+ }
2042
2141
  for (j=0; j<hs->narg; j++) {
2043
2142
  char *base = hs->stage.ptr + hs->off[j];
2044
2143
  if (hs->ptr[j] == NULL) continue;
2045
2144
  if ((const char*)next >= base && (const char*)next + bytes <= base + hs->len[j]) {
2046
2145
  cumo_cuda_runtime_check_status(cumo_cuda_runtime_memcpy_to_pinned(
2047
2146
  (void*)next, hs->ptr[j] + ((const char*)next - LARG(lp,j).ptr), bytes));
2147
+ hs->row_debt += CUMO_NA_STAGE_COPY_COST + bytes;
2148
+ hs->range_debt += CUMO_NA_STAGE_COPY_COST + bytes;
2048
2149
  return;
2049
2150
  }
2050
2151
  }
2051
2152
  }
2052
2153
 
2154
+ int
2155
+ cumo_na_ndloop_writes_existing(cumo_na_loop_t *user)
2156
+ {
2157
+ return ndloop_writes_existing(ndloop_of_user(user));
2158
+ }
2159
+
2160
+ void
2161
+ cumo_na_ndloop_wrote(cumo_na_loop_t *user)
2162
+ {
2163
+ cumo_na_md_loop_t *lp = ndloop_of_user(user);
2164
+ cumo_na_host_stage_t *hs = lp->hs;
2165
+ size_t before = cumo_cuda_runtime_note_device_write();
2166
+
2167
+ if (hs == NULL) return;
2168
+ // The copy holds the element, so the count it adds leaves it current.
2169
+ if (hs->seen_epoch == before) hs->seen_epoch = before + 1;
2170
+ if (hs->row_epoch == before) hs->row_epoch = before + 1;
2171
+ if (hs->epoch == before) hs->epoch = before + 1;
2172
+ }
2173
+
2053
2174
  static void
2054
2175
  ndloop_unstage_host_reads(cumo_na_md_loop_t *lp)
2055
2176
  {
data/lib/cumo/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cumo
4
- VERSION = "0.14.0"
4
+ VERSION = "0.14.1"
5
5
 
6
6
  spec = Gem.loaded_specs["cumo"] if defined?(Gem)
7
7
  REVISION = (spec.metadata["cumo_revision"]&.dup&.freeze if spec && spec.full_gem_path == File.expand_path("../..", __dir__))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cumo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naotoshi Seo
@@ -370,7 +370,7 @@ metadata:
370
370
  changelog_uri: https://github.com/sonots/cumo/blob/master/CHANGELOG.md
371
371
  bug_tracker_uri: https://github.com/sonots/cumo/issues
372
372
  rubygems_mfa_required: 'true'
373
- cumo_revision: v0.13.0-11-gafd8ead9
373
+ cumo_revision: v0.14.1
374
374
  rdoc_options: []
375
375
  require_paths:
376
376
  - lib