carray 3.0.5 → 3.0.6

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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_axis_dispatch.c +6 -0
  6. data/ext/ca_axis_group.c +1 -1
  7. data/ext/ca_categorical_iterator.c +7 -7
  8. data/ext/ca_kernel_iterator.c +116 -231
  9. data/ext/ca_kernel_iterator.h +410 -254
  10. data/ext/ca_obj_bincmp.c +9 -1
  11. data/ext/ca_obj_binop.c +10 -1
  12. data/ext/ca_obj_bitfield.c +2 -0
  13. data/ext/ca_obj_byte_swap.c +2 -0
  14. data/ext/ca_obj_face.c +2 -0
  15. data/ext/ca_obj_fake.c +3 -1
  16. data/ext/ca_obj_grid.c +167 -12
  17. data/ext/ca_obj_meld.c +69 -113
  18. data/ext/ca_obj_moncmp.c +9 -1
  19. data/ext/ca_obj_monop.c +40 -2
  20. data/ext/ca_obj_object.c +54 -20
  21. data/ext/ca_obj_reduce.c +15 -1
  22. data/ext/ca_obj_refer.c +65 -20
  23. data/ext/ca_obj_remap.c +69 -10
  24. data/ext/ca_obj_roll.c +1 -0
  25. data/ext/ca_obj_select.c +4 -1
  26. data/ext/ca_obj_select_axis.c +1 -0
  27. data/ext/ca_obj_shift.c +1 -0
  28. data/ext/ca_obj_stack.c +310 -111
  29. data/ext/ca_obj_stride.c +131 -45
  30. data/ext/ca_obj_string.c +18 -2
  31. data/ext/ca_obj_tile.c +1 -0
  32. data/ext/ca_obj_triop.c +9 -1
  33. data/ext/ca_obj_window.c +1 -0
  34. data/ext/ca_op_ipower.c +8 -8
  35. data/ext/caframe_csv_reader.c +1 -1
  36. data/ext/carray.h +2 -1
  37. data/ext/carray_access.c +27 -64
  38. data/ext/carray_address_basis.c +134 -0
  39. data/ext/carray_attribute.c +8 -0
  40. data/ext/carray_broadcast.c +3 -4
  41. data/ext/carray_build_flags.h +1 -1
  42. data/ext/carray_cast.c +8 -11
  43. data/ext/carray_conversion.c +9 -5
  44. data/ext/carray_core.c +811 -72
  45. data/ext/carray_element.c +13 -9
  46. data/ext/carray_histogram.c +25 -3
  47. data/ext/carray_internal.h +147 -12
  48. data/ext/carray_kernels_bincmp.c +2007 -3
  49. data/ext/carray_kernels_binop.c +25365 -5199
  50. data/ext/carray_kernels_init.c +8 -8
  51. data/ext/carray_kernels_monop.c +10696 -3678
  52. data/ext/carray_kernels_reduce_aggregate.c +4429 -702
  53. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  54. data/ext/carray_kernels_reduce_cumulative.c +1808 -248
  55. data/ext/carray_kernels_reduce_extreme.c +4508 -511
  56. data/ext/carray_kernels_reduce_variance.c +2284 -152
  57. data/ext/carray_kernels_scan.c +240 -240
  58. data/ext/carray_kernels_search.c +4211 -264
  59. data/ext/carray_kernels_sort.c +367 -450
  60. data/ext/carray_lazy.c +47 -0
  61. data/ext/carray_loop.c +1 -1
  62. data/ext/carray_mask.c +130 -139
  63. data/ext/carray_median_percentile.c +4 -4
  64. data/ext/carray_memory_view.c +13 -7
  65. data/ext/carray_operator.c +147 -44
  66. data/ext/carray_order.c +3 -2
  67. data/ext/carray_random.c +4 -1
  68. data/ext/carray_slab.c +97 -21
  69. data/ext/carray_sort.c +1 -1
  70. data/ext/carray_test.c +74 -19
  71. data/ext/carray_time_text.c +2 -2
  72. data/ext/extconf.rb +11 -0
  73. data/ext/mkkernel.rb +346 -94
  74. data/ext/ruby_carray.c +4 -1
  75. data/ext/version.h +4 -4
  76. data/lib/carray/attribute.rb +25 -1
  77. data/lib/carray/autoload_carray.rb +3 -3
  78. data/lib/carray/autoload_method_extension.rb +5 -3
  79. data/lib/carray/axis_group.rb +13 -7
  80. data/lib/carray/basics.rb +1 -1
  81. data/lib/carray/block_iterator.rb +17 -3
  82. data/lib/carray/categorical.rb +6 -8
  83. data/lib/carray/categorical_iterator.rb +2 -2
  84. data/lib/carray/complex.rb +2 -2
  85. data/lib/carray/conditional.rb +3 -2
  86. data/lib/carray/frame/column_verbs.rb +45 -14
  87. data/lib/carray/frame/convert.rb +19 -0
  88. data/lib/carray/frame/csv_parser.rb +32 -32
  89. data/lib/carray/frame/describe.rb +4 -2
  90. data/lib/carray/frame/frame.rb +399 -78
  91. data/lib/carray/frame/group.rb +679 -53
  92. data/lib/carray/frame/io.rb +209 -71
  93. data/lib/carray/frame/join.rb +146 -37
  94. data/lib/carray/frame/records.rb +3 -3
  95. data/lib/carray/frame/reshape.rb +89 -11
  96. data/lib/carray/frame/sort.rb +57 -9
  97. data/lib/carray/frame/verbs.rb +384 -49
  98. data/lib/carray/frame/windows.rb +44 -6
  99. data/lib/carray/inspect.rb +7 -2
  100. data/lib/carray/iterator.rb +32 -0
  101. data/lib/carray/lazy.rb +17 -8
  102. data/lib/carray/mask_gap_fill.rb +37 -17
  103. data/lib/carray/meld_reduce.rb +1 -1
  104. data/lib/carray/methods/choose.rb +3 -2
  105. data/lib/carray/methods/meshgrid.rb +3 -1
  106. data/lib/carray/runtime.rb +47 -8
  107. data/lib/carray/segment_iterator.rb +23 -8
  108. data/lib/carray/serialize.rb +34 -17
  109. data/lib/carray/stack.rb +18 -6
  110. data/lib/carray/time.rb +5 -0
  111. data/lib/carray/window_iterator.rb +16 -4
  112. data/yard-stubs/ca_obj_array.rb +26 -1
  113. data/yard-stubs/carray_cast.rb +8 -5
  114. data/yard-stubs/carray_conversion.rb +8 -2
  115. data/yard-stubs/carray_mask.rb +20 -1
  116. data/yard-stubs/carray_math.rb +31 -16
  117. data/yard-stubs/carray_memory_view.rb +7 -0
  118. data/yard-stubs/carray_test.rb +7 -4
  119. metadata +18 -4
data/ext/mkkernel.rb CHANGED
@@ -328,8 +328,8 @@ module MkKernel
328
328
  # # :match_source (legacy) reads the weight at
329
329
  # # the source type -- wrong for fractional
330
330
  # # weights, kept only for DSL forward-compat.
331
- # shape: :rev5_strict, # W-A1 scalar / W-A2 1-D axis-broadcast / W-A3
332
- # # same-shape (:match_source = W-A3 only)
331
+ # shape: :rev5_strict, # W-A1 scalar / W-A4 shape of the reduced axes /
332
+ # # W-A3 self.shape (:match_source = W-A3 only)
333
333
  # mask: :overlay, # weights.mask OR'd into self.mask before
334
334
  # # kernel call (= legacy parity, Q3 (A))
335
335
  # }
@@ -362,6 +362,13 @@ module MkKernel
362
362
  object_escape: nil,
363
363
  identity_on_empty: false,
364
364
  all_nan_result: nil,
365
+ undefined_if: nil,
366
+ # settled: a C expression over `acc` that is true once no
367
+ # further cell can change the result (any: "acc", all:
368
+ # "!acc"). The slab walk then stops at the first block
369
+ # where it holds (CA_SLAB_REDUCE_T_SETTLE). Only for a
370
+ # single-accumulator reduction without mask_policy.
371
+ settled: nil,
365
372
  outputs: 1,
366
373
  # Two-pass centred algorithm (variance / stddev family).
367
374
  # When algorithm: :two_pass_centred is set, `state / init /
@@ -448,6 +455,10 @@ module MkKernel
448
455
  raise "#{name}: identity_on_empty requires a single-accumulator reduction (sum/prod/count/accumulate/wsum); mean/variance/wmean/minmax are excluded" \
449
456
  unless single_acc && outputs == 1
450
457
  end
458
+ if settled
459
+ raise "#{name}: settled requires a single-accumulator reduction without mask_policy, value_arg or array_arg" \
460
+ unless state.nil? && mask_policy.nil? && value_arg.nil? && array_arg.nil? && outputs == 1
461
+ end
451
462
  # SL.1.0: reduction_kind: licenses a SIMD-friendly contig-branch macro
452
463
  # variant (CA_SLAB_REDUCE_T_{PLUS,MIN,MAX,STAR,VAR}_EX). default
453
464
  # :none keeps the existing CA_SLAB_REDUCE_T_EX path (= unchanged
@@ -491,6 +502,14 @@ module MkKernel
491
502
  raise "#{name}: all_nan_result: :undef needs a mask_policy " \
492
503
  "(UNDEF is written through the mask_policy trigger)"
493
504
  end
505
+ # undefined_if: a C expression over the state that, when true for a
506
+ # slab, leaves the answer undefined though cells contributed -- wmean's
507
+ # weights summing to 0, whose quotient has no value. It is folded into
508
+ # the mask_policy trigger, so the cell is UNDEF (or fill_value).
509
+ if undefined_if && mask_policy.nil?
510
+ raise "#{name}: undefined_if needs a mask_policy " \
511
+ "(UNDEF is written through the mask_policy trigger)"
512
+ end
494
513
  if value_arg
495
514
  raise "#{name}: value_arg must be a Hash" unless value_arg.is_a?(Hash)
496
515
  raise "#{name}: value_arg: target must be :T_IN" unless value_arg[:target] == :T_IN
@@ -570,9 +589,9 @@ module MkKernel
570
589
  raise "#{name}: array_arg: data_type must be :promote or :match_source" \
571
590
  unless [:promote, :match_source].include?(array_arg[:data_type])
572
591
  # PROPOSAL_REDUCTION_PER_FIBER_AUX_OPERAND rev1: shape acceptance expanded
573
- # to W-A1 (scalar) / W-A2 (1-D axis-broadcast) / W-A3 (same shape).
592
+ # to W-A1 (scalar) / W-A4 (shape of the reduced axes) / W-A3 (self.shape).
574
593
  # :match_source = legacy W-A3 only (kept for forward-compat in DSL).
575
- # :rev5_strict = W-A1 + W-A2 + W-A3 acceptance (= new default for wsum/wmean).
594
+ # :rev5_strict = W-A1 + W-A4 + W-A3 acceptance (= new default for wsum/wmean).
576
595
  raise "#{name}: array_arg: shape must be :match_source or :rev5_strict" \
577
596
  unless [:match_source, :rev5_strict].include?(array_arg[:shape])
578
597
  raise "#{name}: array_arg: mask must be :overlay (W.1 MVP)" \
@@ -652,6 +671,8 @@ module MkKernel
652
671
  object_escape: object_escape,
653
672
  identity_on_empty: identity_on_empty,
654
673
  all_nan_result: all_nan_result,
674
+ undefined_if: undefined_if,
675
+ settled: settled,
655
676
  outputs: outputs,
656
677
  algorithm: algorithm,
657
678
  divisor: divisor,
@@ -1057,11 +1078,14 @@ module MkKernel
1057
1078
  # => ...` form).
1058
1079
  #
1059
1080
  # `bind:` defaults to true (= register CArray#<name> + CArray#<name>!).
1081
+ # `bang: false` leaves CArray#<name>! unregistered too, for a method whose
1082
+ # Ruby surface is defined by hand on top of the generated rb_ca_<name> and
1083
+ # rb_ca_<name>_bang.
1060
1084
  # `op:` defaults to name (= the public method name; some mkmath ops
1061
1085
  # use a different operator name from the kernel suffix, e.g. operator
1062
1086
  # methods).
1063
- def self.monop(name, source:, expr:, op: nil, bind: true, cmath: true,
1064
- widening: nil, output: :preserve)
1087
+ def self.monop(name, source:, expr:, op: nil, bind: true, bang: true,
1088
+ cmath: true, widening: nil, output: :preserve)
1065
1089
  op ||= name
1066
1090
  # widening = true => emit wrapper that auto-casts integer input to
1067
1091
  # CA_FLOAT64 (= mkmath monfunc behavior). Default: auto-detect based
@@ -1084,6 +1108,7 @@ module MkKernel
1084
1108
  source: source,
1085
1109
  expr: expr,
1086
1110
  bind: bind,
1111
+ bang: bang,
1087
1112
  cmath: cmath,
1088
1113
  widening: widening,
1089
1114
  output: output,
@@ -1095,8 +1120,10 @@ module MkKernel
1095
1120
  # of integer input to f64 is decided by `monop`'s widening auto-detect
1096
1121
  # based on whether `source:` includes integer data_types — matching mkmath's
1097
1122
  # rule "if no INT_TYPES key, auto-cast; otherwise rely on integer kernel".
1098
- def self.monfunc(name, source:, expr:, op: nil, bind: true, cmath: true)
1099
- monop(name, source: source, expr: expr, op: op, bind: bind, cmath: cmath)
1123
+ def self.monfunc(name, source:, expr:, op: nil, bind: true, bang: true,
1124
+ cmath: true)
1125
+ monop(name, source: source, expr: expr, op: op, bind: bind, bang: bang,
1126
+ cmath: cmath)
1100
1127
  end
1101
1128
 
1102
1129
  # Register an alias of an existing monop method under a different
@@ -1358,6 +1385,12 @@ module MkKernel
1358
1385
  when "_STAR" then "CA_SLAB_REDUCE_STAR8_EX"
1359
1386
  end
1360
1387
  acc_c = resolve_state_type(k[:state][acc_var], oi, si, src)
1388
+ # An 8- or 16-bit integer cell converted to double does not vectorize;
1389
+ # the narrow form sums blocks in int32 first.
1390
+ if macro8 == "CA_SLAB_REDUCE_SUM8_EX" && %i[i8 u8 i16 u16].include?(src) &&
1391
+ acc_c == DTYPES[:f64][:c]
1392
+ macro8 = "CA_SLAB_REDUCE_SUM8_NARROW_EX"
1393
+ end
1361
1394
  expr_macro = "__#{k[:name]}_#{src}_EXPR"
1362
1395
  io.puts "#define #{expr_macro}(__x) ((#{acc_c})(__x))"
1363
1396
  io.puts "#{indent}#{macro8}(#{si[:c]}, #{acc_c}, st, p, m, #{acc_var}, #{acc_init}, #{expr_macro}, #{masked_cnt_var});"
@@ -1877,7 +1910,11 @@ module MkKernel
1877
1910
  out.puts "{"
1878
1911
  out.puts " #{frame} fr;"
1879
1912
  params.each { |_, name| out.puts " fr.#{name} = #{name};" }
1880
- out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1913
+ if params.any? { |type, name| name == "ca" && type.include?("CArray") } && fn.end_with?("_object")
1914
+ out.puts " return ca_iter_ensure_computing(ca, #{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1915
+ else
1916
+ out.puts " return ca_iter_ensure(#{states.size}, fr.states, #{fn}_body, (VALUE) &fr);"
1917
+ end
1881
1918
  out.puts "}"
1882
1919
  out.print rest
1883
1920
  out.string
@@ -2035,6 +2072,7 @@ module MkKernel
2035
2072
  io.puts " if ( naxes != 1 || ! ca_too_few_present(ca->dim[slab_axes[0]], min_count, 0) ) {" if li_gate
2036
2073
  emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
2037
2074
  acc_var, acc_init, reduce_stmt, finish_expr)
2075
+ emit_reduce_short_fiber(io, k, si, oi, src, reduce_stmt, finish_expr)
2038
2076
  io.puts " }" if li_gate
2039
2077
  end
2040
2078
 
@@ -2168,6 +2206,7 @@ module MkKernel
2168
2206
  "#{k[:identity_on_empty] ? 0 : 1})"
2169
2207
  end
2170
2208
  trigger = "(#{trigger}) || #{all_nan_flag}" if all_nan_flag
2209
+ trigger = "(#{trigger}) || (#{k[:undefined_if]})" if k[:undefined_if]
2171
2210
  finish_emit = view_flat \
2172
2211
  ? "(transform_active ? (outer_off + ((ca_size_t)(#{finish_expr})) * axis_vstride) : ((ca_size_t)(#{finish_expr})))" \
2173
2212
  : "(#{finish_expr})"
@@ -2192,7 +2231,9 @@ module MkKernel
2192
2231
  # Non-mask_policy path: the slab macros always take a masked_cnt,
2193
2232
  # so hand them a throwaway. The generic form (suffix "", the
2194
2233
  # :object lane) has a wrapper that hides it.
2195
- if suffix == ""
2234
+ if k[:settled]
2235
+ io.puts " CA_SLAB_REDUCE_T_SETTLE(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt}, #{k[:settled]});"
2236
+ elsif suffix == ""
2196
2237
  io.puts " CA_SLAB_REDUCE_T(#{si[:c]}, st, p, m, #{acc_var}, #{acc_init}, #{reduce_stmt});"
2197
2238
  else
2198
2239
  io.puts " ca_size_t __sr_throwaway_mc = 0;"
@@ -2540,8 +2581,16 @@ module MkKernel
2540
2581
  expr_macro_defs = "#define #{p1_macro}(__x) #{p1_expr}\n" \
2541
2582
  "#define #{p2_macro}(__x) #{p2_expr}\n"
2542
2583
  expr_macro_undefs = "#undef #{p1_macro}\n#undef #{p2_macro}\n"
2543
- p1_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2544
- p2_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{m2_c}, st, p, m, M2, #{m2_init}, #{p2_macro}, __unused_mc);"
2584
+ if %i[i8 u8 i16 u16].include?(src)
2585
+ # Per-cell conversion to double does not vectorize for these widths
2586
+ # (see the CA_SLAB_REDUCE_*_NARROW_EX header comments).
2587
+ q_c = %i[i8 u8].include?(src) ? "int32_t" : "int64_t"
2588
+ p1_reduce_call = "CA_SLAB_REDUCE_SUM8_NARROW_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2589
+ p2_reduce_call = "CA_SLAB_REDUCE_M2_NARROW_EX(#{si[:c]}, #{q_c}, st, p, m, M2, #{m2_init}, mean, #{p2_macro}, __unused_mc);"
2590
+ else
2591
+ p1_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{sum_c}, st, p, m, sum, #{sum_init}, #{p1_macro}, masked_cnt);"
2592
+ p2_reduce_call = "CA_SLAB_REDUCE_SUM8_EX(#{si[:c]}, #{m2_c}, st, p, m, M2, #{m2_init}, #{p2_macro}, __unused_mc);"
2593
+ end
2545
2594
  end
2546
2595
 
2547
2596
  io.puts
@@ -2603,6 +2652,101 @@ module MkKernel
2603
2652
  io.print expr_macro_undefs
2604
2653
  end
2605
2654
 
2655
+ # Short-fiber fast path (shortcut short_fiber_reduce): a single-axis
2656
+ # reduction over the innermost axis of a row-major buffer whose fibers are
2657
+ # short. The general walk asks the kernel iterator for each fiber, and for
2658
+ # fibers of a few cells that per-fiber step costs more than the cells
2659
+ # (1M fibers of 4 float64 cells: 12.6 ms against 1.0 ms for a plain loop,
2660
+ # M2 Max). Here every fiber is folded in place from the buffer, in the
2661
+ # order the general walk folds it, into scalar accumulators. Eligibility
2662
+ # is the loop interchange's (same reduction kinds, no mask, an alias
2663
+ # attach); a fiber longer than 32 cells (16 for min / max) goes to the
2664
+ # general walk, which is already faster than this loop there.
2665
+ def self.emit_reduce_short_fiber(io, k, si, oi, src, reduce_stmt, finish_expr)
2666
+ buf_kinds = %i[plus star min max]
2667
+ rk = k[:reduction_kind]
2668
+ if buf_kinds.include?(rk)
2669
+ acc_vars = [k[:state].keys.first]
2670
+ induction_var = nil
2671
+ else
2672
+ acc_vars = k[:state].keys.select { |v| buf_kinds.include?(rk[v]) }
2673
+ ind_pair = rk.find { |_, kind| kind == :induction }
2674
+ induction_var = ind_pair && ind_pair[0]
2675
+ end
2676
+
2677
+ info = acc_vars.map do |v|
2678
+ { var: v,
2679
+ c_type: resolve_state_type(k[:state][v], oi, si, src),
2680
+ init: resolve_init_expr(k[:init][v], oi, si, src) }
2681
+ end
2682
+
2683
+ reduce_sf = reduce_stmt.dup
2684
+ finish_sf = finish_expr.dup
2685
+ if induction_var
2686
+ ind_pat = Regexp.escape(induction_var.to_s)
2687
+ reduce_sf = reduce_sf.gsub(/\b#{ind_pat}\+\+/, "0")
2688
+ .gsub(/\+\+\b#{ind_pat}\b/, "0")
2689
+ finish_sf = finish_sf.gsub(/\b#{ind_pat}\b/, "((int64_t)__sf_M)")
2690
+ end
2691
+
2692
+ decls = info.map { |i| "#{i[:c_type]} #{i[:var]} = (#{i[:c_type]}) (#{i[:init]});" }
2693
+ .join("\n ")
2694
+
2695
+ # A float min / max whose accumulator is still at the init saw only NaN
2696
+ # or only +/-INFINITY; the fiber is re-read to tell which, as the
2697
+ # general walk's all-NaN rule answers NaN for the first.
2698
+ nan_fixup =
2699
+ if k[:all_nan_result] == :nan && FLOAT_DTYPES.include?(src) && info.size == 1
2700
+ i = info.first
2701
+ <<~C.rstrip
2702
+ if ( #{i[:var]} == (#{i[:c_type]}) (#{i[:init]}) ) {
2703
+ int __sf_seen = 0;
2704
+ for ( ca_size_t __j = 0; __j < __sf_M; __j++ ) __sf_seen |= (__sf_row[__j] == __sf_row[__j]);
2705
+ if ( ! __sf_seen ) #{i[:var]} = (#{i[:c_type]}) NAN;
2706
+ }
2707
+ C
2708
+ else
2709
+ ""
2710
+ end
2711
+
2712
+ # A min / max fold is a compare per cell that the general walk splits
2713
+ # eight ways; past 16 cells that split is faster than this loop.
2714
+ max_m = info.any? { |i| %i[min max].include?(buf_kinds.include?(rk) ? rk : rk[i[:var]]) } ? 16 : 32
2715
+
2716
+ io.puts <<~C
2717
+ if ( naxes == 1
2718
+ && ca->ndim >= 2
2719
+ && slab_axes[0] == (int8_t)(ca->ndim - 1)
2720
+ && ca->dim[ca->ndim - 1] >= 1
2721
+ && ca->dim[ca->ndim - 1] <= #{max_m}
2722
+ && ! ca_has_mask(ca)
2723
+ && ca_attach_is_alias(ca)
2724
+ && ca_fast_path(CA_SC_SHORT_FIBER_REDUCE) ) {
2725
+ ca_size_t __sf_M = ca->dim[ca->ndim - 1];
2726
+ ca_size_t __sf_OUTER = ca->elements / __sf_M;
2727
+ VALUE vout = rb_ca_new_reduced(self, slab_axes, naxes, #{oi[:ca]}, keep_axis);
2728
+ CArray *co;
2729
+ GetCArray(vout, co);
2730
+ #{oi[:c]} *op = (#{oi[:c]} *) co->ptr;
2731
+ ca_attach(ca); /* window: numeric lanes only, nothing raises inside; O(1) alias attach */
2732
+ const #{si[:c]} *p_root = (const #{si[:c]} *) ca->ptr;
2733
+ for ( ca_size_t __sf_o = 0; __sf_o < __sf_OUTER; __sf_o++ ) {
2734
+ const #{si[:c]} *__sf_row = p_root + __sf_o * __sf_M;
2735
+ #{decls}
2736
+ for ( ca_size_t __j = 0; __j < __sf_M; __j++ ) {
2737
+ #{si[:c]} v = __sf_row[__j];
2738
+ #{reduce_sf};
2739
+ (void) v;
2740
+ }
2741
+ #{nan_fixup}
2742
+ op[__sf_o] = (#{oi[:c]}) (#{finish_sf});
2743
+ }
2744
+ ca_detach(ca);
2745
+ return vout;
2746
+ }
2747
+ C
2748
+ end
2749
+
2606
2750
  def self.emit_reduce_loop_interchange(io, k, si, oi, ruby_wrap, src,
2607
2751
  acc_var, acc_init, reduce_stmt, finish_expr)
2608
2752
  # Classify state vars by reduction_kind:
@@ -2752,7 +2896,7 @@ module MkKernel
2752
2896
  && slab_axes[0] != (int8_t)(ca->ndim - 1)
2753
2897
  && ! ca_has_mask(ca)
2754
2898
  && ca_attach_is_alias(ca)
2755
- && ca_fast_paths_enabled() ) {
2899
+ && ca_fast_path(CA_SC_LOOP_INTERCHANGE) ) {
2756
2900
  int8_t __li_ax = slab_axes[0];
2757
2901
  ca_size_t __li_M = ca->dim[__li_ax];
2758
2902
  ca_size_t __li_INNER = 1;
@@ -2803,7 +2947,7 @@ module MkKernel
2803
2947
  && ! ca_has_mask(ca)
2804
2948
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
2805
2949
  && ((CAStack *) ca)->k_axis == 0
2806
- && ca_fast_paths_enabled() ) {
2950
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
2807
2951
  CAStack *__li_st = (CAStack *) ca;
2808
2952
  int8_t __li_ax = slab_axes[0];
2809
2953
  ca_size_t __li_M = ca->dim[__li_ax];
@@ -2831,7 +2975,7 @@ module MkKernel
2831
2975
  ca_size_t __li_parent_out = __li_OUTER * __li_INNER;
2832
2976
  const ca_size_t __li_TILE = 512;
2833
2977
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2834
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
2978
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
2835
2979
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2836
2980
  #{oi[:c]} *__li_op_k = op + (ca_size_t) __kk * __li_parent_out;
2837
2981
  #{core.call("p_root", "__li_op_k", "__li_OUTER")}
@@ -2869,7 +3013,7 @@ module MkKernel
2869
3013
  && ((CAStack *) ca)->k_axis > 0
2870
3014
  && ( slab_axes[0] > ((CAStack *) ca)->k_axis
2871
3015
  || slab_axes[0] + 1 == ((CAStack *) ca)->k_axis )
2872
- && ca_fast_paths_enabled() ) {
3016
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
2873
3017
  CAStack *__li_st = (CAStack *) ca;
2874
3018
  int8_t __li_ax = slab_axes[0];
2875
3019
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -2926,7 +3070,7 @@ module MkKernel
2926
3070
  ca_size_t __li_plane_elems = __li_M * __li_INNER;
2927
3071
  const ca_size_t __li_TILE = 512;
2928
3072
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
2929
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
3073
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
2930
3074
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
2931
3075
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
2932
3076
 
@@ -3012,7 +3156,7 @@ module MkKernel
3012
3156
  && ca_func[ca->obj_type].attach == ca_stack_func.attach
3013
3157
  && ((CAStack *) ca)->k_axis > 0
3014
3158
  && slab_axes[0] + 1 < ((CAStack *) ca)->k_axis
3015
- && ca_fast_paths_enabled() ) {
3159
+ && ca_fast_path(CA_SC_STACK_LOOP_INTERCHANGE) ) {
3016
3160
  CAStack *__li_st = (CAStack *) ca;
3017
3161
  int8_t __li_ax = slab_axes[0];
3018
3162
  int8_t __li_kax = (int8_t) __li_st->k_axis;
@@ -3084,7 +3228,7 @@ module MkKernel
3084
3228
 
3085
3229
  const ca_size_t __li_TILE = 512;
3086
3230
  for ( int32_t __kk = 0; __kk < __li_st->n_parents; __kk++ ) {
3087
- ca_attach(__li_st->parents[__kk]); /* window: numeric lanes only, nothing raises inside */
3231
+ ca_attach(__li_st->parents[__kk]); /* window: a parent may run Ruby to fill itself (a CAObject's copy_data); a raising attach holds nothing and the earlier parents are already detached, and the numeric loop after it does not raise */
3088
3232
  const #{si[:c]} *p_root = (const #{si[:c]} *) __li_st->parents[__kk]->ptr;
3089
3233
  ca_size_t __li_k_base = (ca_size_t) __kk * __li_k_stride;
3090
3234
 
@@ -3394,7 +3538,7 @@ module MkKernel
3394
3538
  io.puts " ca->elements > 0 &&"
3395
3539
  io.puts " ! ca_too_few_present(ca->elements, min_count, 0) &&" if k[:mask_policy] == :min_count
3396
3540
  io.puts " ca_is_lazy_view(ca) && ! ca_has_mask(ca) &&"
3397
- io.puts " ca_fast_paths_enabled() ) {"
3541
+ io.puts " ca_fast_path(CA_SC_LAZY_STREAMING_REDUCE) ) {"
3398
3542
  io.puts " #{fn}_t __ctx;"
3399
3543
  io.puts " __ctx.chunk = NULL;"
3400
3544
  io.puts " __ctx.ca = ca;"
@@ -3513,12 +3657,12 @@ module MkKernel
3513
3657
  # computation type (so a float weight against an int source is not
3514
3658
  # truncated -- the body reads it as `double`); :match_source keeps the
3515
3659
  # legacy source-type storage. waca is the CA_* constant the three
3516
- # materialization sites (W-A1 / W-A2 / W-A3) allocate/wrap against, and
3660
+ # materialization sites (W-A1 / W-A4 / W-A3) allocate/wrap against, and
3517
3661
  # must agree with the T_W the reduce macro reads (double for :promote).
3518
3662
  waca = (k[:array_arg][:data_type] == :promote) ? "CA_FLOAT64" : "src->data_type"
3519
3663
  # PROPOSAL_REDUCTION_PER_FIBER_AUX_OPERAND rev1: weights acceptance
3520
- # expanded to 3 forms (W-A1 scalar / W-A2 1-D axis-broadcast / W-A3
3521
- # full per-element). W-A1 / W-A2 are materialized to W-A3 form
3664
+ # expanded to 3 forms (W-A1 scalar / W-A4 shape of the reduced axes / W-A3
3665
+ # full per-element). W-A1 / W-A4 are materialized to W-A3 form
3522
3666
  # at dispatch (= simple, uniform kernel path). rev5 strict acceptance
3523
3667
  # mirrors PROPOSAL_LINEAR_INTERP_PER_FIBER_MATCHED rev5 in spirit.
3524
3668
  io.puts " if ( argc < 1 ) {"
@@ -3526,13 +3670,18 @@ module MkKernel
3526
3670
  io.puts " }"
3527
3671
  io.puts " volatile VALUE rweights = argv[0];"
3528
3672
  io.puts " argc--; argv++;"
3529
- # rev5 A1: convert a single-element CArray (CScalar / [1] 1-D / all
3530
- # dim==1) to a Ruby Numeric and route it through the scalar path.
3673
+ # A CScalar is a scalar weight. Any other array, one with a single
3674
+ # element included, is held to the shape rules below.
3531
3675
  io.puts " if ( rb_obj_is_carray(rweights) ) {"
3532
3676
  io.puts " CArray *cv_pre_;"
3533
3677
  io.puts " GetCArray(rweights, cv_pre_);"
3534
- io.puts " if ( cv_pre_->elements == 1 ) {"
3678
+ io.puts " if ( ca_is_scalar(cv_pre_) ) {"
3679
+ io.puts " int8_t wtype_ = cv_pre_->data_type;"
3535
3680
  io.puts " rweights = rb_funcall(rweights, rb_intern(\"[]\"), 1, INT2NUM(0));"
3681
+ # A boolean cell reads as true / false; as a weight it is 1 / 0.
3682
+ io.puts " if ( wtype_ == CA_BOOLEAN && rweights != CA_UNDEF ) {"
3683
+ io.puts " rweights = INT2FIX(RTEST(rweights) ? 1 : 0);"
3684
+ io.puts " }"
3536
3685
  io.puts " }"
3537
3686
  io.puts " }"
3538
3687
  # The weights are attached from here to the end of the call, so they
@@ -3545,7 +3694,16 @@ module MkKernel
3545
3694
  wio.puts " /* W-A1: Ruby Numeric scalar -> full-shape constant weight CArray */"
3546
3695
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3547
3696
  wio.puts " GetCArray(vcw, cw); /* a new entity: its ptr needs no attach */"
3548
- wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3697
+ wio.puts " if ( rweights == CA_UNDEF ) {"
3698
+ wio.puts " /* A masked weight (UNDEF, or a one-element array masked): every"
3699
+ wio.puts " cell's weight is masked, so no cell contributes. */"
3700
+ wio.puts " memset(cw->ptr, 0, (size_t) cw->elements * cw->bytes);"
3701
+ wio.puts " ca_create_mask(cw);"
3702
+ wio.puts " memset(cw->mask->ptr, 1, (size_t) cw->elements);"
3703
+ wio.puts " }"
3704
+ wio.puts " else {"
3705
+ wio.puts " rb_ca_obj2ptr(vcw, rweights, cw->ptr); /* fill first cell */"
3706
+ wio.puts " }"
3549
3707
  wio.puts " /* broadcast first cell to all elements (= constant fill) */"
3550
3708
  wio.puts " {"
3551
3709
  wio.puts " char *p0 = cw->ptr;"
@@ -3557,72 +3715,75 @@ module MkKernel
3557
3715
  wio.puts " else {"
3558
3716
  wio.puts " /* CArray val: coerce to the weight storage type (f64 for :promote, so a"
3559
3717
  wio.puts " float weight against an int source is not truncated) */"
3560
- wio.puts " /* vcv holds the view while cv is read: W-A2 below moves vcw on to"
3718
+ wio.puts " /* vcv holds the view while cv is read: W-A4 below moves vcw on to"
3561
3719
  wio.puts " the materialised copy before it is done with cv. */"
3562
3720
  wio.puts " volatile VALUE vcv = rb_ca_wrap_readonly(rweights, INT2NUM(#{waca}));"
3563
3721
  wio.puts " CArray *cv;"
3564
3722
  wio.puts " GetCArray(vcv, cv);"
3565
3723
  wio.puts " vcw = vcv;"
3566
3724
  wio.puts ""
3567
- wio.puts " /* rev5 form detection: A3 (same ndim) -> A2 (1-D axis-broadcast) -> raise */"
3568
- wio.puts " if ( cv->ndim == src->ndim ) {"
3569
- wio.puts " /* W-A3 commit-or-raise: shape strict match */"
3570
- wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3571
- wio.puts " if ( cv->dim[i] != src->dim[i] ) {"
3572
- wio.puts %Q[ rb_raise(rb_eArgError, "%s: weights shape mismatch (dim[%d]=%ld != self.dim[%d]=%ld; expected a scalar, a 1-D array along the reduced axis, or self.shape)", ca_calling_method_name(), (int)i, (long)cv->dim[i], (int)i, (long)src->dim[i]);]
3573
- wio.puts " }"
3574
- wio.puts " }"
3575
- wio.puts " /* W-A3 path: use cv directly (= existing) */"
3725
+ wio.puts " /* form detection: W-A3 (self.shape) -> W-A4 (shape of the reduced axes,"
3726
+ wio.puts " taken in ascending axis order whatever order axis: lists them) -> raise */"
3727
+ wio.puts " int8_t wax[CA_RANK_MAX];"
3728
+ wio.puts " for ( int8_t i = 0; i < naxes; i++ ) {"
3729
+ wio.puts " int8_t a = slab_axes[i], j = i;"
3730
+ wio.puts " while ( j > 0 && wax[j-1] > a ) { wax[j] = wax[j-1]; j--; }"
3731
+ wio.puts " wax[j] = a;"
3732
+ wio.puts " }"
3733
+ wio.puts " int is_full_shape = ( cv->ndim == src->ndim );"
3734
+ wio.puts " for ( int8_t i = 0; is_full_shape && i < src->ndim; i++ ) {"
3735
+ wio.puts " if ( cv->dim[i] != src->dim[i] ) is_full_shape = 0;"
3736
+ wio.puts " }"
3737
+ wio.puts " int is_reduced_shape = ( ! is_full_shape && naxes >= 1 && cv->ndim == naxes );"
3738
+ wio.puts " for ( int8_t i = 0; is_reduced_shape && i < naxes; i++ ) {"
3739
+ wio.puts " if ( cv->dim[i] != src->dim[wax[i]] ) is_reduced_shape = 0;"
3740
+ wio.puts " }"
3741
+ wio.puts " if ( is_full_shape ) {"
3742
+ wio.puts " /* W-A3: per-element weights, used as they are */"
3576
3743
  wio.puts " cw = cv;"
3577
3744
  wio.puts " }"
3578
- wio.puts " else if ( naxes == 1 && cv->ndim == 1 && cv->dim[0] == src->dim[slab_axes[0]] ) {"
3579
- wio.puts " /* W-A2: 1-D axis-broadcast -> materialize to full shape. cw and"
3580
- wio.puts " its mask are made before cv is attached: making them allocates. */"
3745
+ wio.puts " else if ( is_reduced_shape ) {"
3746
+ wio.puts " /* W-A4: one weight per cell of the reduced axes, repeated along the"
3747
+ wio.puts " kept axes; materialized to self.shape. cw and its mask are made"
3748
+ wio.puts " before cv is attached: making them allocates. */"
3581
3749
  wio.puts " vcw = rb_carray_new(#{waca}, src->ndim, src->dim, 0, NULL);"
3582
3750
  wio.puts " GetCArray(vcw, cw);"
3583
3751
  wio.puts " if ( ca_has_mask(cv) ) {"
3584
3752
  wio.puts " ca_create_mask(cw);"
3585
3753
  wio.puts " }"
3586
3754
  wio.puts " ca_attach(cv); /* window: nothing raises inside */"
3587
- wio.puts " /* broadcast cv (1-D, length src->dim[slab_axes[0]]) along reduce axis */"
3588
3755
  wio.puts " {"
3589
- wio.puts " int8_t baxis = slab_axes[0];"
3590
- wio.puts " ca_size_t inner_stride = 1;"
3591
- wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3592
- wio.puts " ca_size_t axis_n = src->dim[baxis];"
3593
- wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3594
- wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3595
- wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3596
- wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3597
- wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3598
- wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + a * cv->bytes, cw->bytes);"
3599
- wio.puts " }"
3600
- wio.puts " }"
3756
+ wio.puts " ca_size_t idx[CA_RANK_MAX], wstep[CA_RANK_MAX];"
3757
+ wio.puts " ca_size_t step = 1;"
3758
+ wio.puts " boolean8_t *cw_m = ca_has_mask(cv) ? (boolean8_t *) cw->mask->ptr : NULL;"
3759
+ wio.puts " boolean8_t *cv_m = ca_has_mask(cv) ? (boolean8_t *) cv->mask->ptr : NULL;"
3760
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) {"
3761
+ wio.puts " idx[i] = 0;"
3762
+ wio.puts " wstep[i] = 0; /* kept axes: the weight repeats */"
3601
3763
  wio.puts " }"
3602
- wio.puts " }"
3603
- wio.puts " /* Propagate cv mask via W-A3 overlay path below (= mask materialize) */"
3604
- wio.puts " if ( ca_has_mask(cv) ) {"
3605
- wio.puts " /* broadcast cv mask along axis */"
3606
- wio.puts " boolean8_t *cw_m = (boolean8_t *)cw->mask->ptr;"
3607
- wio.puts " boolean8_t *cv_m = (boolean8_t *)cv->mask->ptr;"
3608
- wio.puts " int8_t baxis = slab_axes[0];"
3609
- wio.puts " ca_size_t inner_stride = 1;"
3610
- wio.puts " for ( int8_t ii = baxis + 1; ii < src->ndim; ii++ ) inner_stride *= src->dim[ii];"
3611
- wio.puts " ca_size_t axis_n = src->dim[baxis];"
3612
- wio.puts " ca_size_t outer = ( axis_n && inner_stride ) ? src->elements / (axis_n * inner_stride) : 0; /* a zero-length axis leaves nothing to fill */"
3613
- wio.puts " for ( ca_size_t o = 0; o < outer; o++ ) {"
3614
- wio.puts " for ( ca_size_t a = 0; a < axis_n; a++ ) {"
3615
- wio.puts " for ( ca_size_t in = 0; in < inner_stride; in++ ) {"
3616
- wio.puts " ca_size_t flat = (o * axis_n + a) * inner_stride + in;"
3617
- wio.puts " cw_m[flat] = cv_m[a];"
3618
- wio.puts " }"
3764
+ wio.puts " for ( int8_t i = naxes - 1; i >= 0; i-- ) {"
3765
+ wio.puts " wstep[wax[i]] = step;"
3766
+ wio.puts " step *= cv->dim[i];"
3767
+ wio.puts " }"
3768
+ wio.puts " for ( ca_size_t flat = 0; flat < src->elements; flat++ ) {"
3769
+ wio.puts " ca_size_t w = 0;"
3770
+ wio.puts " for ( int8_t i = 0; i < src->ndim; i++ ) w += idx[i] * wstep[i];"
3771
+ wio.puts " memcpy(cw->ptr + flat * cw->bytes, cv->ptr + w * cv->bytes, cw->bytes);"
3772
+ wio.puts " if ( cw_m ) cw_m[flat] = cv_m[w];"
3773
+ wio.puts " for ( int8_t i = src->ndim - 1; i >= 0; i-- ) {"
3774
+ wio.puts " if ( ++idx[i] < src->dim[i] ) break;"
3775
+ wio.puts " idx[i] = 0;"
3619
3776
  wio.puts " }"
3620
3777
  wio.puts " }"
3621
3778
  wio.puts " }"
3622
3779
  wio.puts " ca_detach(cv);"
3623
3780
  wio.puts " }"
3624
3781
  wio.puts " else {"
3625
- wio.puts %Q[ rb_raise(rb_eArgError, "%s: w shape not accepted (val.ndim=%d, val.dim[0]=%ld; expected scalar / [M=%ld] / self.shape)", ca_calling_method_name(), (int)cv->ndim, (long)(cv->ndim >= 1 ? cv->dim[0] : 0), (long)(naxes == 1 ? src->dim[slab_axes[0]] : -1));]
3782
+ wio.puts " volatile VALUE rexpect = rb_ary_new();"
3783
+ wio.puts " for ( int8_t i = 0; i < naxes; i++ ) {"
3784
+ wio.puts " rb_ary_push(rexpect, SIZE2NUM(src->dim[wax[i]]));"
3785
+ wio.puts " }"
3786
+ wio.puts %Q[ rb_raise(rb_eArgError, "%s: weights of shape %"PRIsVALUE" not accepted (expected a scalar, the shape of the reduced axes %"PRIsVALUE", or self.shape %"PRIsVALUE")", ca_calling_method_name(), rb_funcall(vcv, rb_intern("shape"), 0), rexpect, rb_funcall(self, rb_intern("shape"), 0));]
3626
3787
  wio.puts " }"
3627
3788
  wio.puts " }"
3628
3789
  # Mask overlay (= legacy W-A3 path; if the materialized cw carries a mask, overlay it)
@@ -5116,15 +5277,16 @@ module MkKernel
5116
5277
  # The object lane calls Ruby for every cell of the case B/C walk, which
5117
5278
  # runs between a direct attach and detach of self and val; a raise there
5118
5279
  # would leave both attached. An entity holds no attach, so the lane
5119
- # reads an entity copy of whichever is a view.
5280
+ # reads an entity copy of whichever is a view, or an entity that leaves
5281
+ # ptr NULL at rest (a CASource).
5120
5282
  entity_bc = if is_object
5121
5283
  <<~C.chomp
5122
5284
  volatile VALUE vca_entity = self;
5123
- if ( ! ca_is_entity(ca) ) {
5285
+ if ( ! ca_is_entity(ca) || ! ca->ptr ) {
5124
5286
  vca_entity = rb_ca_copy(self);
5125
5287
  GetCArray(vca_entity, ca);
5126
5288
  }
5127
- if ( ! ca_is_entity(cv) ) {
5289
+ if ( ! ca_is_entity(cv) || ! cv->ptr ) {
5128
5290
  vsrc_val = rb_ca_copy(vsrc_val);
5129
5291
  GetCArray(vsrc_val, cv);
5130
5292
  }
@@ -6051,6 +6213,9 @@ module MkKernel
6051
6213
  # `op_powi_<type>` resolves to `op_powi_int8_t` for int8 source).
6052
6214
  # Substitution uses math_suffix (= typedef name, matches the suffix
6053
6215
  # used in op_powi macro instantiations in ca_op_powi.h).
6216
+ # An object body calls Ruby on every cell and does not vectorize; the
6217
+ # pragma there only adds a "loop not vectorized" warning.
6218
+ simd = ( src == :object ) ? "" : "#pragma omp simd"
6054
6219
  expr_body = raw.gsub(/#1/, "*p1").gsub(/#2/, "*p2").gsub(/#3/, "*p3").gsub(/<type>/, suffix)
6055
6220
  # Option B+C pilot (binop emit substrate hardening, MEMO_BINOP_STRIDED_AUDIT.md):
6056
6221
  # The no-mask body is split into a contig branch (i1==i2==i3==1) using the
@@ -6103,7 +6268,7 @@ module MkKernel
6103
6268
  #{c_type} * __restrict__ r1 = q1;
6104
6269
  #{c_type} * __restrict__ r2 = q2;
6105
6270
  #{c_type} * __restrict__ r3 = q3;
6106
- #pragma omp simd
6271
+ #{simd}
6107
6272
  for (k=0; k<n; k++) {
6108
6273
  p1 = r1 + k; p2 = r2 + k; p3 = r3 + k;
6109
6274
  { #{expr_body} }
@@ -6111,7 +6276,7 @@ module MkKernel
6111
6276
  }
6112
6277
  else if ( ( apart1 || a1 == a3 ) && ( apart2 || a2 == a3 ) ) {
6113
6278
  /* in place: an iteration reads and writes only its own cell */
6114
- #pragma omp simd
6279
+ #{simd}
6115
6280
  for (k=0; k<n; k++) {
6116
6281
  p1 = q1 + k; p2 = q2 + k; p3 = q3 + k;
6117
6282
  { #{expr_body} }
@@ -6123,6 +6288,66 @@ module MkKernel
6123
6288
  { #{expr_body} }
6124
6289
  }
6125
6290
  }
6291
+ } else if ( i3 == 1 && ( ( i1 == 1 && i2 == 0 ) || ( i1 == 0 && i2 == 1 ) ) ) {
6292
+ /* scalar branch: one operand is a scalar (stride 0). It is read
6293
+ into a local before the loop, so a store to the output cannot
6294
+ change it and the loop vectorizes like the contig branch. */
6295
+ #{c_type} sv = ( i1 == 0 ) ? *q1 : *q2;
6296
+ #{c_type} *qa = ( i1 == 0 ) ? q2 : q1;
6297
+ #{c_type} *p1, *p2, *p3;
6298
+ uintptr_t aa = (uintptr_t) qa, a3 = (uintptr_t) q3;
6299
+ uintptr_t nb = (uintptr_t) n * sizeof(*q3);
6300
+ (void)p1; (void)p2; (void)p3;
6301
+ if ( i1 == 0 ) {
6302
+ p1 = &sv;
6303
+ if ( a3 + nb <= aa || aa + nb <= a3 ) {
6304
+ #{c_type} * __restrict__ ra = qa;
6305
+ #{c_type} * __restrict__ r3 = q3;
6306
+ #{simd}
6307
+ for (k=0; k<n; k++) {
6308
+ p2 = ra + k; p3 = r3 + k;
6309
+ { #{expr_body} }
6310
+ }
6311
+ }
6312
+ else if ( aa == a3 ) {
6313
+ #{simd}
6314
+ for (k=0; k<n; k++) {
6315
+ p2 = qa + k; p3 = q3 + k;
6316
+ { #{expr_body} }
6317
+ }
6318
+ }
6319
+ else {
6320
+ for (k=0; k<n; k++) {
6321
+ p2 = qa + k; p3 = q3 + k;
6322
+ { #{expr_body} }
6323
+ }
6324
+ }
6325
+ }
6326
+ else {
6327
+ p2 = &sv;
6328
+ if ( a3 + nb <= aa || aa + nb <= a3 ) {
6329
+ #{c_type} * __restrict__ ra = qa;
6330
+ #{c_type} * __restrict__ r3 = q3;
6331
+ #{simd}
6332
+ for (k=0; k<n; k++) {
6333
+ p1 = ra + k; p3 = r3 + k;
6334
+ { #{expr_body} }
6335
+ }
6336
+ }
6337
+ else if ( aa == a3 ) {
6338
+ #{simd}
6339
+ for (k=0; k<n; k++) {
6340
+ p1 = qa + k; p3 = q3 + k;
6341
+ { #{expr_body} }
6342
+ }
6343
+ }
6344
+ else {
6345
+ for (k=0; k<n; k++) {
6346
+ p1 = qa + k; p3 = q3 + k;
6347
+ { #{expr_body} }
6348
+ }
6349
+ }
6350
+ }
6126
6351
  } else {
6127
6352
  /* strided branch: numo-shape `p += s` increment form. */
6128
6353
  #{c_type} *p1 = q1, *p2 = q2, *p3 = q3;
@@ -6455,6 +6680,24 @@ module MkKernel
6455
6680
  { #{expr_body} }
6456
6681
  }
6457
6682
  }
6683
+ } else if ( i3 == 1 && i1 == 1 && i2 == 0 ) {
6684
+ /* scalar on the right: read it once, so the boolean store
6685
+ cannot be taken to change it and the loop vectorizes */
6686
+ #{c_type} sv = *q2;
6687
+ p2 = &sv;
6688
+ for (k=0; k<n; k++) {
6689
+ p1 = q1 + k;
6690
+ p3 = q3 + k;
6691
+ { #{expr_body} }
6692
+ }
6693
+ } else if ( i3 == 1 && i1 == 0 && i2 == 1 ) {
6694
+ #{c_type} sv = *q1;
6695
+ p1 = &sv;
6696
+ for (k=0; k<n; k++) {
6697
+ p2 = q2 + k;
6698
+ p3 = q3 + k;
6699
+ { #{expr_body} }
6700
+ }
6458
6701
  } else {
6459
6702
  for (k=0; k<n; k++) {
6460
6703
  p1 = q1 + k*i1;
@@ -6730,7 +6973,7 @@ module MkKernel
6730
6973
  # surface) + optional CAMath module function, NOT as `<name>_ki`.
6731
6974
  if k[:kind] == :monop
6732
6975
  io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:op]}", rb_ca_#{k[:name]}, 0);] if k[:bind]
6733
- io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}!", rb_ca_#{k[:name]}_bang, 0);]
6976
+ io.puts %Q[ rb_define_method(rb_cCArray, "#{k[:name]}!", rb_ca_#{k[:name]}_bang, 0);] if k[:bang]
6734
6977
  io.puts %Q[ rb_define_module_function(rb_mCAMath, "#{k[:name]}", rb_cmath_#{k[:name]}, 1);] if k[:cmath]
6735
6978
  return
6736
6979
  end
@@ -6958,11 +7201,10 @@ MkKernel.reduce :max,
6958
7201
  # its identity value (1 for all, 0 for any) for those cells. An all-
6959
7202
  # masked slab returns the identity (all([]) == true, any([]) == false).
6960
7203
  #
6961
- # Early-break is intentionally NOT implemented; idempotent acc &= v /
6962
- # acc |= v is SIMD-friendly and the branch-free form usually wins on
6963
- # modern CPUs. For dramatically lopsided inputs (= mostly-false `all`
6964
- # or mostly-true `any`) a profile-driven early-break variant can be
6965
- # added later as a separate kernel.
7204
+ # The walk stops once the answer is settled (settled:): any at the first
7205
+ # true, all and none at the first false / true. The test runs between
7206
+ # blocks of branch-free `acc |= v` / `acc &= v`, so a slab that never
7207
+ # settles keeps the speed of the plain fold.
6966
7208
  MkKernel.header_block <<~C
6967
7209
  /* The square root of a variance: an ε-negative from rounding becomes 0,
6968
7210
  a NaN stays NaN (fmax(NaN, 0.0) is 0.0). */
@@ -6986,6 +7228,8 @@ C
6986
7228
  MkKernel.reduce :all,
6987
7229
  init: "1",
6988
7230
  reduce: "acc &= v",
7231
+ settled: "!acc",
7232
+ reduction_kind: :min,
6989
7233
  source: [:bool],
6990
7234
  output: :bool,
6991
7235
  ruby_scalar: :BOOL2VAL,
@@ -6995,6 +7239,8 @@ MkKernel.reduce :all,
6995
7239
  MkKernel.reduce :any,
6996
7240
  init: "0",
6997
7241
  reduce: "acc |= v",
7242
+ settled: "acc",
7243
+ reduction_kind: :max,
6998
7244
  source: [:bool],
6999
7245
  output: :bool,
7000
7246
  ruby_scalar: :BOOL2VAL,
@@ -7009,6 +7255,8 @@ MkKernel.reduce :any,
7009
7255
  MkKernel.reduce :none,
7010
7256
  init: "1",
7011
7257
  reduce: "acc &= !v",
7258
+ settled: "!acc",
7259
+ reduction_kind: :min,
7012
7260
  source: [:bool],
7013
7261
  output: :bool,
7014
7262
  ruby_scalar: :BOOL2VAL,
@@ -7457,9 +7705,9 @@ MkKernel.reduce :wsum,
7457
7705
  # framework as wsum, but uses state+finish form (= sum + den) for the
7458
7706
  # normalisation `mean = Σv*w / Σw` semantic.
7459
7707
  #
7460
- # Edge case: if all valid weights sum to zero (= den == 0 but some cells
7461
- # valid), finish evaluates 0/0 -> NaN. Legacy proc_wmean had the same
7462
- # undefined behavior (no division check); mkkernel preserves it.
7708
+ # Weights summing to zero (den == 0 with cells present, weights of 0 or of
7709
+ # both signs) leave the mean undefined: the cell is UNDEF, as for a slab
7710
+ # with no valid cell, rather than 0/0 = NaN or x/0 = +-Infinity.
7463
7711
  #
7464
7712
  # 3.0 breaking semantic changes vs legacy (= identical to wsum W.2):
7465
7713
  # - calling convention positional -> kwargs (axes positional, opts kw)
@@ -7476,6 +7724,7 @@ MkKernel.reduce :wmean,
7476
7724
  reduce: "(sum += (double) v * (double) w, den += (double) w)",
7477
7725
  reduction_kind: { sum: :plus, den: :plus }, # SL.1.4b
7478
7726
  finish: "sum / den",
7727
+ undefined_if: "den == 0",
7479
7728
  source: MkKernel::ALL_NUMERIC,
7480
7729
  output: :f64,
7481
7730
  ruby_scalar: :rb_float_new,
@@ -8632,7 +8881,11 @@ MkKernel.monfunc :floor,
8632
8881
  object: '(#2) = rb_funcall((#1), rb_intern("floor"), 0);',
8633
8882
  }
8634
8883
 
8884
+ # round: the Ruby methods `round(ndigits)` / `round!(ndigits)` are defined in
8885
+ # carray_round.c on top of this kernel, so none is registered here.
8635
8886
  MkKernel.monfunc :round,
8887
+ bind: false,
8888
+ bang: false,
8636
8889
  source: MkKernel::ALL_NUMERIC + [:object],
8637
8890
  expr: {
8638
8891
  int: "(#2) = (#1);",
@@ -8952,22 +9205,23 @@ MkKernel.monfunc :rad_pi,
8952
9205
  # Float NaN included), maximum / minimum let it win. In both flavours, of
8953
9206
  # two zeros the max is +0 and the min is -0 in either order. C's fmax /
8954
9207
  # fmin leave that choice to the library (glibc and macOS answer
8955
- # differently), so the float branch does not call them.
9208
+ # differently), so the float branch does not call them. The sign of a
9209
+ # zero is read with copysign(1, x) rather than signbit(x): GCC does not
9210
+ # vectorize signbit on a double, which left the float64 kernels 17x
9211
+ # slower than float32. NaN is handled before, so copysign never sees one.
8956
9212
  MkKernel.binop :pmax,
8957
9213
  source: MkKernel::ALL_NUMERIC + [:object],
8958
9214
  expr: {
8959
9215
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8960
- float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8961
9216
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern(">")) ? (#2) : (#1);',
8962
- }
9217
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) > (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) > 0)) ? (#1) : (#2);"))
8963
9218
 
8964
9219
  MkKernel.binop :pmin,
8965
9220
  source: MkKernel::ALL_NUMERIC + [:object],
8966
9221
  expr: {
8967
9222
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8968
- float: "(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8969
9223
  object: '(#3) = ca_obj_extremum_replaces((#2), (#1), rb_intern("<")) ? (#2) : (#1);',
8970
- }
9224
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#2) : isnan(#2) ? (#1) : ((#1) < (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) < 0)) ? (#1) : (#2);"))
8971
9225
 
8972
9226
  # NaN-propagate variants. Float
8973
9227
  # branch tests both operands for NaN and short-circuits to NaN; non-
@@ -8978,17 +9232,15 @@ MkKernel.binop :maximum,
8978
9232
  source: MkKernel::ALL_NUMERIC + [:object],
8979
9233
  expr: {
8980
9234
  int: "(#3) = (#1) > (#2) ? (#1) : (#2);",
8981
- float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && !signbit(#1))) ? (#1) : (#2);",
8982
9235
  object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) > 0 ? (#2) : (#1);',
8983
- }
9236
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) > (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) > 0)) ? (#1) : (#2);"))
8984
9237
 
8985
9238
  MkKernel.binop :minimum,
8986
9239
  source: MkKernel::ALL_NUMERIC + [:object],
8987
9240
  expr: {
8988
9241
  int: "(#3) = (#1) < (#2) ? (#1) : (#2);",
8989
- float: "(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && signbit(#1))) ? (#1) : (#2);",
8990
9242
  object: '(#3) = CA_OBJ_ISNAN(#1) ? (#1) : CA_OBJ_ISNAN(#2) ? (#2) : ca_obj_cmp((#2), (#1)) < 0 ? (#2) : (#1);',
8991
- }
9243
+ }.merge(MkKernel.float_widths("(#3) = isnan(#1) ? (#1) : isnan(#2) ? (#2) : ((#1) < (#2) || ((#1) == (#2) && copysign<f>(1, (#1)) < 0)) ? (#1) : (#2);"))
8992
9244
 
8993
9245
  # + and - work on the parts independently, so one generic expression
8994
9246
  # covers every numeric data_type at any width.