convolver 3.0.1 → 4.0.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 +4 -4
- data/CHANGELOG.md +56 -3
- data/README.md +79 -119
- data/docs/rules.md +206 -0
- data/docs/terminology.md +30 -0
- data/ext/convolver/convolve_raw.c +51 -165
- data/ext/convolver/convolve_raw.h +5 -19
- data/ext/convolver/convolver.c +42 -20
- data/ext/convolver/correlate_raw.c +51 -161
- data/ext/convolver/correlate_raw.h +5 -19
- data/ext/convolver/extconf.rb +1 -0
- data/ext/convolver/fft_shape.c +116 -0
- data/ext/convolver/fft_shape.h +8 -0
- data/ext/convolver/raw_config.h +11 -0
- data/ext/convolver/raw_helpers.h +71 -0
- data/ext/convolver/raw_loop.h +93 -0
- data/lib/convolver/buffer_preparation.rb +4 -4
- data/lib/convolver/circular_fft_operation.rb +4 -3
- data/lib/convolver/cost_profiles.rb +44 -0
- data/lib/convolver/fft_cost.rb +52 -0
- data/lib/convolver/fft_estimator.rb +25 -32
- data/lib/convolver/fft_limits.rb +1 -1
- data/lib/convolver/linear_fft_operation.rb +4 -2
- data/lib/convolver/operation_dtype.rb +42 -0
- data/lib/convolver/operation_execution.rb +56 -45
- data/lib/convolver/operation_options.rb +4 -5
- data/lib/convolver/operation_plan.rb +17 -4
- data/lib/convolver/preparation_cost.rb +46 -0
- data/lib/convolver/real_fft_shape.rb +2 -61
- data/lib/convolver/signal_extension.rb +2 -2
- data/lib/convolver/version.rb +1 -1
- data/lib/convolver.rb +183 -68
- metadata +12 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#include "fft_shape.h"
|
|
2
|
+
#include "raw_config.h"
|
|
3
|
+
#include <limits.h>
|
|
4
|
+
#include <math.h>
|
|
5
|
+
|
|
6
|
+
static size_t positive_size(VALUE value) {
|
|
7
|
+
if (!RB_INTEGER_TYPE_P(value) || RTEST(rb_funcall(value, rb_intern("<="), 1, INT2FIX(0)))) {
|
|
8
|
+
rb_raise(rb_eArgError, "FFT dimensions and size limit must be positive integers");
|
|
9
|
+
}
|
|
10
|
+
return NUM2SIZET(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static long read_shape(VALUE shape, size_t *dimensions) {
|
|
14
|
+
Check_Type(shape, T_ARRAY);
|
|
15
|
+
long rank = RARRAY_LEN(shape);
|
|
16
|
+
if (rank < 1 || rank > LARGEST_RANK) {
|
|
17
|
+
rb_raise(rb_eArgError, "FFT shape rank must be between 1 and %d", LARGEST_RANK);
|
|
18
|
+
}
|
|
19
|
+
for (long axis = 0; axis < rank; axis++) {
|
|
20
|
+
dimensions[axis] = positive_size(rb_ary_entry(shape, axis));
|
|
21
|
+
}
|
|
22
|
+
return rank;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static size_t doubled_candidate(size_t initial, size_t target, int even, size_t maximum) {
|
|
26
|
+
size_t candidate = initial;
|
|
27
|
+
while (candidate < target || (even && candidate % 2 != 0)) {
|
|
28
|
+
if (candidate > maximum / 2) return 0;
|
|
29
|
+
candidate *= 2;
|
|
30
|
+
}
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Enumerate 3/5 powers only while they can improve the best 2/3/5 candidate. */
|
|
35
|
+
static size_t next_fast_size(size_t target, int even, size_t maximum) {
|
|
36
|
+
size_t best = 0, five = 1;
|
|
37
|
+
for (;;) {
|
|
38
|
+
size_t three = five;
|
|
39
|
+
for (;;) {
|
|
40
|
+
size_t candidate = doubled_candidate(three, target, even, maximum);
|
|
41
|
+
if (candidate != 0 && (best == 0 || candidate < best)) best = candidate;
|
|
42
|
+
size_t bound = best != 0 ? best : maximum;
|
|
43
|
+
if (three > bound / 3) break;
|
|
44
|
+
three *= 3;
|
|
45
|
+
}
|
|
46
|
+
size_t bound = best != 0 ? best : maximum;
|
|
47
|
+
if (five > bound / 5) break;
|
|
48
|
+
five *= 5;
|
|
49
|
+
}
|
|
50
|
+
return best;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Generates optional smooth dimensions without allocating numeric buffers.
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
static VALUE convolver_fft_fast_shape(VALUE self, VALUE shape, VALUE maximum_value) {
|
|
58
|
+
(void)self;
|
|
59
|
+
size_t dimensions[LARGEST_RANK];
|
|
60
|
+
long rank = read_shape(shape, dimensions);
|
|
61
|
+
size_t maximum = positive_size(maximum_value);
|
|
62
|
+
for (long axis = 0; axis < rank; axis++) {
|
|
63
|
+
if (dimensions[axis] > maximum) return Qnil;
|
|
64
|
+
dimensions[axis] = next_fast_size(dimensions[axis], axis == rank - 1, maximum);
|
|
65
|
+
if (dimensions[axis] == 0) return Qnil;
|
|
66
|
+
}
|
|
67
|
+
VALUE result = rb_ary_new_capa(rank);
|
|
68
|
+
for (long axis = 0; axis < rank; axis++) {
|
|
69
|
+
rb_ary_push(result, SIZET2NUM(dimensions[axis]));
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static double large_factor_penalty(size_t value) {
|
|
75
|
+
double penalty = 0.0;
|
|
76
|
+
size_t divisor = 2;
|
|
77
|
+
while (divisor <= value / divisor) {
|
|
78
|
+
while (value % divisor == 0) {
|
|
79
|
+
if (divisor > 7) penalty += 1.5 * (log2((double)divisor) - 3.0);
|
|
80
|
+
value /= divisor;
|
|
81
|
+
}
|
|
82
|
+
divisor += divisor == 2 ? 1 : 2;
|
|
83
|
+
}
|
|
84
|
+
if (value > 7) penalty += 1.5 * (log2((double)value) - 3.0);
|
|
85
|
+
return penalty;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* Scores an admissible candidate; Ruby owns the complete FFT buffer policy.
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
static VALUE convolver_fft_shape_cost(VALUE self, VALUE shape) {
|
|
93
|
+
(void)self;
|
|
94
|
+
size_t dimensions[LARGEST_RANK], product = 1;
|
|
95
|
+
long rank = read_shape(shape, dimensions);
|
|
96
|
+
for (long axis = 0; axis < rank; axis++) {
|
|
97
|
+
/* Bound trial factorization even if this private primitive is called directly. */
|
|
98
|
+
if (dimensions[axis] > (size_t)INT_MAX / 16) {
|
|
99
|
+
rb_raise(rb_eRangeError, "FFT scoring axis exceeds native integer limit");
|
|
100
|
+
}
|
|
101
|
+
if (dimensions[axis] > SIZE_MAX / product) {
|
|
102
|
+
rb_raise(rb_eRangeError, "FFT scoring size exceeds native element limit");
|
|
103
|
+
}
|
|
104
|
+
product *= dimensions[axis];
|
|
105
|
+
}
|
|
106
|
+
double cost = 0.0;
|
|
107
|
+
for (long axis = 0; axis < rank; axis++) {
|
|
108
|
+
cost += log2((double)dimensions[axis]) + large_factor_penalty(dimensions[axis]);
|
|
109
|
+
}
|
|
110
|
+
return DBL2NUM((double)product * cost);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void convolver_init_fft_shape(VALUE convolver) {
|
|
114
|
+
rb_define_singleton_method(convolver, "fft_fast_shape", convolver_fft_fast_shape, 2);
|
|
115
|
+
rb_define_singleton_method(convolver, "fft_shape_cost", convolver_fft_shape_cost, 1);
|
|
116
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#ifndef CONVOLVER_RAW_HELPERS_H
|
|
2
|
+
#define CONVOLVER_RAW_HELPERS_H
|
|
3
|
+
|
|
4
|
+
static inline size_t checked_add( size_t left, size_t right, const char *description ) {
|
|
5
|
+
if ( right > SIZE_MAX - left ) {
|
|
6
|
+
rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
|
|
7
|
+
}
|
|
8
|
+
return left + right;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static inline size_t checked_multiply( size_t left, size_t right, const char *description ) {
|
|
12
|
+
if ( left != 0 && right > SIZE_MAX / left ) {
|
|
13
|
+
rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
|
|
14
|
+
}
|
|
15
|
+
return left * right;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static inline size_t size_from_shape( int rank, const size_t *shape, const char *description ) {
|
|
19
|
+
size_t size = 1;
|
|
20
|
+
int i;
|
|
21
|
+
for ( i = 0; i < rank; i++ ) {
|
|
22
|
+
size = checked_multiply( size, shape[i], description );
|
|
23
|
+
}
|
|
24
|
+
return size;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Sets reverse indices
|
|
28
|
+
static inline void corner_reset( int rank, const size_t *shape, size_t *rev_indices ) {
|
|
29
|
+
int i;
|
|
30
|
+
for ( i = 0; i < rank; i++ ) { rev_indices[i] = shape[i] - 1; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Counts indices down, returns number of ranks that reset
|
|
34
|
+
static inline int corner_dec( int rank, const size_t *shape, size_t *rev_indices ) {
|
|
35
|
+
int i = 0;
|
|
36
|
+
(void) rank;
|
|
37
|
+
while ( ! rev_indices[i]-- ) {
|
|
38
|
+
rev_indices[i] = shape[i] - 1;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return i;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Generates co-increment steps by rank boundaries crossed, for the outer position as inner position is incremented by 1
|
|
45
|
+
static inline void calc_co_increment(
|
|
46
|
+
int rank, const size_t *outer_shape, const size_t *inner_shape, size_t *co_increment ) {
|
|
47
|
+
size_t factor = 1;
|
|
48
|
+
int i;
|
|
49
|
+
co_increment[0] = 1; // co-increment is always 1 in lowest rank
|
|
50
|
+
for ( i = 0; i < rank; i++ ) {
|
|
51
|
+
size_t skipped = checked_multiply( factor, outer_shape[i] - inner_shape[i], "array offset" );
|
|
52
|
+
co_increment[i+1] = checked_add( co_increment[i], skipped, "array offset" );
|
|
53
|
+
factor = checked_multiply( factor, outer_shape[i], "array size" );
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static inline size_t maximum_offset(
|
|
58
|
+
int rank, const size_t *outer_shape, const size_t *inner_shape, const char *description ) {
|
|
59
|
+
size_t factor = 1;
|
|
60
|
+
size_t offset = 0;
|
|
61
|
+
int i;
|
|
62
|
+
for ( i = 0; i < rank; i++ ) {
|
|
63
|
+
size_t dimension_offset = checked_multiply( factor, inner_shape[i] - 1, description );
|
|
64
|
+
offset = checked_add( offset, dimension_offset, description );
|
|
65
|
+
factor = checked_multiply( factor, outer_shape[i], "array size" );
|
|
66
|
+
}
|
|
67
|
+
return offset;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
#endif
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Intentionally included once per dtype; operation/dtype dispatch is outside hot loops. */
|
|
2
|
+
void RAW_NAME(
|
|
3
|
+
int in_rank, const size_t *in_shape, const RAW_TYPE *in_ptr,
|
|
4
|
+
int kernel_rank, const size_t *kernel_shape, const RAW_TYPE *kernel_ptr,
|
|
5
|
+
int out_rank, const size_t *out_shape, RAW_TYPE *out_ptr ) {
|
|
6
|
+
size_t i, j, input_size, kernel_size, kernel_aligned, out_size, offset;
|
|
7
|
+
size_t maximum_input_offset;
|
|
8
|
+
size_t out_co_incr[LARGEST_RANK + 1], kernel_co_incr[LARGEST_RANK + 1];
|
|
9
|
+
size_t ker_q[LARGEST_RANK], out_q[LARGEST_RANK];
|
|
10
|
+
size_t *kernel_co_incr_cache;
|
|
11
|
+
VALUE cache_storage = 0;
|
|
12
|
+
|
|
13
|
+
kernel_size = size_from_shape( kernel_rank, kernel_shape, "kernel size" );
|
|
14
|
+
kernel_aligned = kernel_size - kernel_size % RAW_LANES;
|
|
15
|
+
out_size = size_from_shape( out_rank, out_shape, "output size" );
|
|
16
|
+
input_size = size_from_shape( in_rank, in_shape, "input size" );
|
|
17
|
+
|
|
18
|
+
calc_co_increment( in_rank, in_shape, out_shape, out_co_incr );
|
|
19
|
+
calc_co_increment( in_rank, in_shape, kernel_shape, kernel_co_incr );
|
|
20
|
+
maximum_input_offset = checked_add(
|
|
21
|
+
maximum_offset( in_rank, in_shape, out_shape, "input offset" ),
|
|
22
|
+
maximum_offset( in_rank, in_shape, kernel_shape, "input offset" ),
|
|
23
|
+
"input offset"
|
|
24
|
+
);
|
|
25
|
+
if ( maximum_input_offset >= input_size ) {
|
|
26
|
+
rb_raise( rb_eRangeError, "input offset exceeds native implementation limit" );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
kernel_co_incr_cache = RB_ALLOCV_N( size_t, cache_storage, kernel_size );
|
|
30
|
+
kernel_co_incr_cache[0] = 0;
|
|
31
|
+
|
|
32
|
+
corner_reset( kernel_rank, kernel_shape, ker_q );
|
|
33
|
+
for ( i = 1; i < kernel_size; i++ ) {
|
|
34
|
+
kernel_co_incr_cache[i] = checked_add(
|
|
35
|
+
kernel_co_incr_cache[i-1],
|
|
36
|
+
kernel_co_incr[ corner_dec( kernel_rank, kernel_shape, ker_q ) ],
|
|
37
|
+
"kernel offset"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
offset = 0;
|
|
42
|
+
corner_reset( out_rank, out_shape, out_q );
|
|
43
|
+
|
|
44
|
+
for ( i = 0; i < out_size; i++ ) {
|
|
45
|
+
RAW_TYPE t = 0;
|
|
46
|
+
#if RAW_SIMD
|
|
47
|
+
RAW_VECTOR simd_t = RAW_ZERO();
|
|
48
|
+
RAW_TYPE v[RAW_LANES];
|
|
49
|
+
for ( j = 0; j < kernel_aligned; j += RAW_LANES ) {
|
|
50
|
+
RAW_VECTOR simd_x = RAW_LOAD(kernel_ptr + j);
|
|
51
|
+
#if RAW_LANES == 4
|
|
52
|
+
RAW_VECTOR simd_y = _mm_set_ps(
|
|
53
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+3)]],
|
|
54
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+2)]],
|
|
55
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+1)]],
|
|
56
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]]);
|
|
57
|
+
#else
|
|
58
|
+
RAW_VECTOR simd_y = _mm_set_pd(
|
|
59
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+1)]],
|
|
60
|
+
in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]]);
|
|
61
|
+
#endif
|
|
62
|
+
simd_t = RAW_ADD(RAW_MUL(simd_x, simd_y), simd_t);
|
|
63
|
+
}
|
|
64
|
+
RAW_STORE(v, simd_t);
|
|
65
|
+
#else
|
|
66
|
+
for ( j = 0; j < kernel_aligned; j++ ) {
|
|
67
|
+
t += in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]] * kernel_ptr[j];
|
|
68
|
+
}
|
|
69
|
+
#endif
|
|
70
|
+
for ( j = kernel_aligned; j < kernel_size; j++ ) {
|
|
71
|
+
t += in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]] * kernel_ptr[j];
|
|
72
|
+
}
|
|
73
|
+
#if RAW_SIMD
|
|
74
|
+
#if RAW_LANES == 4
|
|
75
|
+
out_ptr[i] = v[0] + v[1] + v[2] + v[3] + t;
|
|
76
|
+
#else
|
|
77
|
+
out_ptr[i] = v[0] + v[1] + t;
|
|
78
|
+
#endif
|
|
79
|
+
#else
|
|
80
|
+
out_ptr[i] = t;
|
|
81
|
+
#endif
|
|
82
|
+
|
|
83
|
+
if ( i + 1 < out_size ) {
|
|
84
|
+
offset = checked_add(
|
|
85
|
+
offset,
|
|
86
|
+
out_co_incr[ corner_dec( out_rank, out_shape, out_q ) ],
|
|
87
|
+
"input offset"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
RB_ALLOCV_END( cache_storage );
|
|
93
|
+
}
|
|
@@ -35,15 +35,15 @@ module Convolver
|
|
|
35
35
|
attr_reader :plan, :signal_size, :kernel_size, :limits
|
|
36
36
|
|
|
37
37
|
def common!
|
|
38
|
-
limits.buffer!(plan.result_size,
|
|
39
|
-
limits.buffer!(signal_size,
|
|
40
|
-
limits.buffer!(kernel_size,
|
|
38
|
+
limits.buffer!(plan.result_size, plan.dtype::ELEMENT_BYTE_SIZE, 'result buffer')
|
|
39
|
+
limits.buffer!(signal_size, plan.dtype::ELEMENT_BYTE_SIZE, 'signal conversion buffer')
|
|
40
|
+
limits.buffer!(kernel_size, plan.dtype::ELEMENT_BYTE_SIZE, 'kernel conversion buffer')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def extension!
|
|
44
44
|
return if plan.valid?
|
|
45
45
|
|
|
46
|
-
limits.buffer!(plan.extended_size,
|
|
46
|
+
limits.buffer!(plan.extended_size, plan.dtype::ELEMENT_BYTE_SIZE, 'extended signal buffer')
|
|
47
47
|
return if plan.boundary == :constant
|
|
48
48
|
|
|
49
49
|
plan.extended_shape.each do |length|
|
|
@@ -5,6 +5,7 @@ module Convolver
|
|
|
5
5
|
# @private
|
|
6
6
|
class CircularFftOperation
|
|
7
7
|
def initialize(operation, signal, kernel, plan)
|
|
8
|
+
@dtype = plan.dtype
|
|
8
9
|
@real_axis = plan.fft_buffers.real_axis
|
|
9
10
|
@operation = operation
|
|
10
11
|
@signal = Numo::DFloat.cast(signal)
|
|
@@ -28,7 +29,7 @@ module Convolver
|
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
|
-
attr_reader :operation, :signal, :kernel, :real_axis
|
|
32
|
+
attr_reader :operation, :signal, :kernel, :real_axis, :dtype
|
|
32
33
|
|
|
33
34
|
def real_product(real_axis)
|
|
34
35
|
final_axis = signal.ndim - 1
|
|
@@ -36,7 +37,7 @@ module Convolver
|
|
|
36
37
|
prepared_kernel = prepare_real_operand(kernel, real_axis, final_axis)
|
|
37
38
|
result = real_spectrum_product(prepared_signal, prepared_kernel)
|
|
38
39
|
result = result.swapaxes(real_axis, final_axis) unless real_axis == final_axis
|
|
39
|
-
|
|
40
|
+
dtype.cast(result)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def prepare_real_operand(value, real_axis, final_axis)
|
|
@@ -55,7 +56,7 @@ module Convolver
|
|
|
55
56
|
kernel_spectrum = Numo::Pocketfft.fftn(kernel)
|
|
56
57
|
kernel_spectrum.inplace.conj if operation == :correlation
|
|
57
58
|
result = Numo::Pocketfft.ifftn(signal_spectrum.inplace * kernel_spectrum).real
|
|
58
|
-
|
|
59
|
+
dtype.cast(result)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def fold_kernel(source, signal_shape, anchors)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Immutable measured-environment coefficients in seconds; ranks one through three.
|
|
5
|
+
# @private
|
|
6
|
+
module CostProfiles
|
|
7
|
+
MAC = {
|
|
8
|
+
direct: [[6.55e-6, 3.24e-10, 7.08e-10].freeze, [6.55e-6, 3.24e-10, 7.08e-10].freeze].freeze,
|
|
9
|
+
fixed: [1.64e-5, 5.12e-5, 1.13e-4].freeze,
|
|
10
|
+
transform: [8.02e-10, 7.17e-10, 1.54e-9].freeze,
|
|
11
|
+
indices: { reflect: 1.33e-7, nearest: 1.46e-7, mirror: 1.57e-7, wrap: 3.23e-8 }.freeze,
|
|
12
|
+
folding: [3.78e-7, 3.99e-7, 5.09e-7].freeze
|
|
13
|
+
}.freeze
|
|
14
|
+
X86 = {
|
|
15
|
+
direct: [[1.59e-5, 3.08e-10, 2.52e-10].freeze, [1.63e-5, 3.22e-10, 3.96e-10].freeze].freeze,
|
|
16
|
+
fixed: [3.77e-5, 1.20e-4, 1.98e-4].freeze,
|
|
17
|
+
transform: [2.00e-9, 2.13e-9, 3.13e-9].freeze,
|
|
18
|
+
indices: { reflect: 2.32e-7, nearest: 2.88e-7, mirror: 2.62e-7, wrap: 1.11e-7 }.freeze,
|
|
19
|
+
folding: [8.20e-7, 7.51e-7, 9.79e-7].freeze
|
|
20
|
+
}.freeze
|
|
21
|
+
ARM = {
|
|
22
|
+
direct: [[1.88e-5, 5.12e-10, 5.96e-10].freeze, [1.88e-5, 5.12e-10, 5.96e-10].freeze].freeze,
|
|
23
|
+
fixed: [4.20e-5, 1.20e-4, 2.70e-4].freeze,
|
|
24
|
+
transform: [3.29e-9, 2.89e-9, 3.74e-9].freeze,
|
|
25
|
+
indices: { reflect: 2.62e-7, nearest: 3.67e-7, mirror: 2.87e-7, wrap: 1.50e-7 }.freeze,
|
|
26
|
+
folding: [9.97e-7, 8.80e-7, 1.24e-6].freeze
|
|
27
|
+
}.freeze
|
|
28
|
+
def self.for_platform(platform)
|
|
29
|
+
case platform
|
|
30
|
+
when /\Aarm64-darwin/ then MAC
|
|
31
|
+
when /\Ax86_64-linux/ then X86
|
|
32
|
+
when /\Aaarch64-linux/ then ARM
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
CURRENT = for_platform(RUBY_PLATFORM)
|
|
37
|
+
|
|
38
|
+
def self.for_rank(rank)
|
|
39
|
+
CURRENT if (1..3).cover?(rank)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_constant :CostProfiles
|
|
44
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/preparation_cost'
|
|
4
|
+
|
|
5
|
+
module Convolver
|
|
6
|
+
# Shared transform costs and a lower bound that never constructs an FFT plan.
|
|
7
|
+
# @private
|
|
8
|
+
class FftCost
|
|
9
|
+
REAL = { 1 => 1.0e-9, 2 => 1.0e-9, 3 => 1.5e-9 }.freeze
|
|
10
|
+
COMPLEX = { 1 => 2.4e-9, 2 => 2.4e-9, 3 => 2.8e-9 }.freeze
|
|
11
|
+
FIXED = { 0 => 5.0e-6, 1 => 1.5e-5, 2 => 3.0e-5, 3 => 1.0e-4 }.freeze
|
|
12
|
+
PREPARATION = 1.0e-9
|
|
13
|
+
|
|
14
|
+
def self.fixed(rank, profile: nil)
|
|
15
|
+
return profile.fetch(:fixed).fetch(rank - 1) if profile
|
|
16
|
+
|
|
17
|
+
FIXED.fetch(rank, FIXED.values.last)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.transform(size, rank, real: true, profile: nil)
|
|
21
|
+
coefficient(rank, real:, profile:) * size * Math.log([size, 2].max)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.lower_bound(plan, kernel_shape, threshold:)
|
|
25
|
+
rank = kernel_shape.length
|
|
26
|
+
profile = plan.cost_profile
|
|
27
|
+
setup = fixed(rank, profile:)
|
|
28
|
+
return setup if setup >= threshold || plan.wrap? || rank.zero?
|
|
29
|
+
|
|
30
|
+
size = minimum_size(plan.extended_shape, kernel_shape)
|
|
31
|
+
setup + transform(size, rank, profile:) + (2 * PREPARATION * size) + PreparationCost.new(plan).indices
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.coefficient(rank, real:, profile:)
|
|
35
|
+
return profile.fetch(:transform).fetch(rank - 1) * (real ? 1 : 2.2) if profile
|
|
36
|
+
|
|
37
|
+
costs = real ? REAL : COMPLEX
|
|
38
|
+
costs.fetch(rank, costs.values.last)
|
|
39
|
+
end
|
|
40
|
+
private_class_method :coefficient
|
|
41
|
+
|
|
42
|
+
def self.minimum_size(signal_shape, kernel_shape)
|
|
43
|
+
# Ruby integers avoid overflow: this is only a cost bound, never an allocation.
|
|
44
|
+
shape = signal_shape.zip(kernel_shape).map { |signal, kernel| signal + kernel - 1 }
|
|
45
|
+
shape[-1] += 1 if shape[-1].odd?
|
|
46
|
+
shape.reduce(1, :*)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :minimum_size
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private_constant :FftCost
|
|
52
|
+
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'convolver/fft_cost'
|
|
4
|
+
|
|
3
5
|
module Convolver
|
|
4
6
|
# Estimates the selected real or complex PocketFFT implementation cost.
|
|
5
7
|
# @private
|
|
6
8
|
class FftEstimator
|
|
7
|
-
REAL_FFT_COST = { 1 => 1.0e-9, 2 => 9.0e-10, 3 => 1.45e-9 }.freeze
|
|
8
|
-
COMPLEX_FFT_COST = { 1 => 2.4e-9, 2 => 2.4e-9, 3 => 2.8e-9 }.freeze
|
|
9
|
-
FIXED_COST = 1.5e-4
|
|
10
|
-
PREPARATION_COST = 1.0e-9
|
|
11
9
|
AXIS_MOVE_COST = 1.6e-9
|
|
10
|
+
RESULT_COST = 5.0e-10
|
|
12
11
|
CORRELATION_COST = 2.0e-10
|
|
13
12
|
|
|
14
13
|
def initialize(operation, signal, kernel, plan)
|
|
@@ -16,54 +15,48 @@ module Convolver
|
|
|
16
15
|
@signal = signal
|
|
17
16
|
@kernel = kernel
|
|
18
17
|
@plan = plan
|
|
18
|
+
@profile = plan.cost_profile
|
|
19
19
|
@buffers = plan.fft_buffers
|
|
20
|
+
@preparation = PreparationCost.new(plan)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def call
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
axis_move_cost = AXIS_MOVE_COST * moved_size
|
|
27
|
-
correlation_cost = operation == :correlation ? CORRELATION_COST * spectrum_size : 0.0
|
|
28
|
-
FIXED_COST + transform_cost + preparation_cost + axis_move_cost + correlation_cost
|
|
24
|
+
return FftCost.fixed(0) + preparation.conversion + result_cost if signal.ndim.zero?
|
|
25
|
+
|
|
26
|
+
transform_cost + preparation_cost + extra_cost
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
private
|
|
32
30
|
|
|
33
|
-
attr_reader :operation, :signal, :kernel, :plan, :buffers
|
|
31
|
+
attr_reader :operation, :signal, :kernel, :plan, :buffers, :preparation
|
|
34
32
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
def transform_cost
|
|
34
|
+
transform = FftCost.transform(buffers.transform_size, signal.ndim,
|
|
35
|
+
real: !buffers.real_axis.nil?, profile: @profile)
|
|
36
|
+
FftCost.fixed(signal.ndim, profile: @profile) + transform + (AXIS_MOVE_COST * moved_size)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
def extra_cost
|
|
40
|
+
correlation_cost = operation == :correlation ? CORRELATION_COST * buffers.spectrum_size : 0.0
|
|
41
|
+
correlation_cost + preparation.conversion + result_cost
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
cost_for(REAL_FFT_COST), 0]
|
|
44
|
+
def result_cost
|
|
45
|
+
RESULT_COST * plan.result_size * (plan.dtype::ELEMENT_BYTE_SIZE / 4)
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
working_size = plan.wrap? ? signal.size + kernel.size : 2 * transform_size
|
|
54
|
-
extension_size + working_size
|
|
55
|
-
end
|
|
48
|
+
def preparation_cost
|
|
49
|
+
return circular_preparation_cost if plan.wrap?
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
costs.fetch(signal.ndim, costs.values.last)
|
|
51
|
+
(2 * FftCost::PREPARATION * buffers.transform_size) + preparation.extension
|
|
59
52
|
end
|
|
60
53
|
|
|
61
|
-
def
|
|
62
|
-
|
|
54
|
+
def circular_preparation_cost
|
|
55
|
+
(FftCost::PREPARATION * (signal.size + kernel.size)) + preparation.folding(kernel.size)
|
|
63
56
|
end
|
|
64
57
|
|
|
65
|
-
def moved_size
|
|
66
|
-
real_axis && real_axis != signal.ndim - 1 ? 3 * signal.size : 0
|
|
58
|
+
def moved_size
|
|
59
|
+
buffers.real_axis && buffers.real_axis != signal.ndim - 1 ? 3 * signal.size : 0
|
|
67
60
|
end
|
|
68
61
|
end
|
|
69
62
|
|
data/lib/convolver/fft_limits.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Convolver
|
|
|
5
5
|
# @private
|
|
6
6
|
class LinearFftOperation
|
|
7
7
|
def initialize(operation, signal, kernel, plan)
|
|
8
|
+
@dtype = plan.dtype
|
|
8
9
|
@operation = operation
|
|
9
10
|
@signal = signal
|
|
10
11
|
@kernel = kernel
|
|
@@ -15,12 +16,13 @@ module Convolver
|
|
|
15
16
|
signal_padded = zero_padded(signal)
|
|
16
17
|
kernel_padded = zero_padded(kernel)
|
|
17
18
|
full_result = real_spectrum_product(signal_padded, kernel_padded)
|
|
18
|
-
|
|
19
|
+
cropped = full_result[*valid_ranges]
|
|
20
|
+
dtype == Numo::DFloat ? cropped.dup : dtype.cast(cropped)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
|
-
attr_reader :operation, :signal, :kernel, :transform_shape
|
|
25
|
+
attr_reader :operation, :signal, :kernel, :transform_shape, :dtype
|
|
24
26
|
|
|
25
27
|
def zero_padded(value)
|
|
26
28
|
Numo::DFloat.zeros(*transform_shape).tap do |padded|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Resolves public precision without allocating operand copies.
|
|
5
|
+
# @private
|
|
6
|
+
class OperationDtype
|
|
7
|
+
SINGLE_INPUTS = [Numo::SFloat, Numo::Int8, Numo::UInt8, Numo::Int16, Numo::UInt16].freeze
|
|
8
|
+
DOUBLE_INPUTS = [Numo::DFloat, Numo::Int32, Numo::UInt32, Numo::Int64, Numo::UInt64].freeze
|
|
9
|
+
INPUTS = (SINGLE_INPUTS + DOUBLE_INPUTS).freeze
|
|
10
|
+
OUTPUTS = [Numo::SFloat, Numo::DFloat].freeze
|
|
11
|
+
|
|
12
|
+
def self.resolve(signal, kernel, dtype)
|
|
13
|
+
validate_inputs!(signal, kernel)
|
|
14
|
+
validate_dtype!(dtype)
|
|
15
|
+
types = [signal.class, kernel.class]
|
|
16
|
+
dtype || (types.any? { |type| DOUBLE_INPUTS.include?(type) } ? Numo::DFloat : Numo::SFloat)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate_inputs!(signal, kernel)
|
|
20
|
+
unless signal.is_a?(Numo::NArray) && kernel.is_a?(Numo::NArray)
|
|
21
|
+
raise ArgumentError, 'signal and kernel must be Numo::NArray values'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return if [signal.class, kernel.class].all? { |type| INPUTS.include?(type) }
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, 'unsupported signal or kernel dtype'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.validate_dtype!(dtype)
|
|
30
|
+
return if dtype.nil? || OUTPUTS.include?(dtype)
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, 'dtype must be Numo::SFloat, Numo::DFloat, or nil'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.cast_fill(value, dtype)
|
|
36
|
+
# A scalar IEEE float conversion, without allocating a Numo buffer before preflight.
|
|
37
|
+
dtype == Numo::SFloat ? [value].pack('f').unpack1('f') : value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_constant :OperationDtype
|
|
42
|
+
end
|