grx-tensor 0.1.0 → 0.2.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 +26 -0
- data/GUIA_PRINCIPIANTES.md +1046 -0
- data/README.es.md +1199 -0
- data/README.md +1026 -283
- data/ext/grx/extconf.rb +4 -18
- data/ext/grx/grx_core.c +411 -331
- data/ext/grx/grx_core.h +23 -13
- data/ext/unix/Makefile +3 -27
- data/ext/windows/Makefile.mingw +3 -23
- data/grx-tensor.gemspec +41 -35
- data/lib/grx/c_api.rb +75 -39
- data/lib/grx/data.rb +87 -0
- data/lib/grx/loss.rb +55 -25
- data/lib/grx/nn.rb +160 -41
- data/lib/grx/optim.rb +21 -16
- data/lib/grx/serialization.rb +66 -0
- data/lib/grx/storage.rb +19 -27
- data/lib/grx/tensor.rb +234 -65
- data/lib/grx/utils.rb +43 -0
- data/lib/grx/version.rb +1 -1
- data/lib/grx.rb +22 -6
- metadata +37 -28
data/lib/grx/storage.rb
CHANGED
|
@@ -4,46 +4,39 @@ require "fiddle"
|
|
|
4
4
|
|
|
5
5
|
module GRX
|
|
6
6
|
# ===================================================================
|
|
7
|
-
# Storage —
|
|
7
|
+
# Storage — Native memory buffer
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
# @ptr
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# When CAPI is loaded:
|
|
10
|
+
# @ptr -> Fiddle::Pointer to 32-byte aligned doubles block
|
|
11
|
+
# allocated via grx_alloc() (C posix_memalign / _aligned_malloc).
|
|
12
|
+
# Data lives in C heap, NOT managed by Ruby GC.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# @data
|
|
16
|
-
#
|
|
17
|
-
# La separación entre los dos modos es transparente para Tensor.
|
|
14
|
+
# When CAPI is NOT loaded (fallback):
|
|
15
|
+
# @data -> Standard Ruby Array (slow but correct).
|
|
18
16
|
# ===================================================================
|
|
19
17
|
class Storage
|
|
20
18
|
attr_reader :size
|
|
21
|
-
|
|
22
|
-
# ptr expuesto para que CAPI pueda leerlo directamente
|
|
23
19
|
attr_reader :ptr
|
|
24
20
|
|
|
25
21
|
def initialize(array_plano)
|
|
26
|
-
|
|
22
|
+
flat = array_plano.is_a?(Array) ? array_plano.flatten : Array(array_plano)
|
|
23
|
+
@size = flat.size
|
|
27
24
|
|
|
28
25
|
if CAPI::LOADED
|
|
29
|
-
#
|
|
30
|
-
# grx_alloc devuelve un double* alineado a 32 bytes
|
|
26
|
+
# Fast mode: aligned C memory
|
|
31
27
|
@ptr = CAPI.grx_alloc(@size)
|
|
32
|
-
raise StorageError, "grx_alloc
|
|
28
|
+
raise StorageError, "grx_alloc failed (OOM)" if @ptr.null?
|
|
33
29
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
bytes = array_plano.pack("d*")
|
|
30
|
+
# Pack Ruby Array into C buffer as IEEE 754 doubles
|
|
31
|
+
bytes = flat.map(&:to_f).pack("d*")
|
|
37
32
|
@ptr[0, bytes.bytesize] = bytes
|
|
38
33
|
|
|
39
|
-
#
|
|
40
|
-
# Ruby sea recolectado por el GC. Usamos ObjectSpace para evitar
|
|
41
|
-
# que el closure capture self (lo que impediría la recolección).
|
|
34
|
+
# Finalizer releases C memory upon Ruby GC collection
|
|
42
35
|
ptr_to_free = @ptr
|
|
43
36
|
ObjectSpace.define_finalizer(self, self.class.make_finalizer(ptr_to_free))
|
|
44
37
|
else
|
|
45
|
-
#
|
|
46
|
-
@data =
|
|
38
|
+
# Fallback mode: Ruby Array
|
|
39
|
+
@data = flat.map(&:to_f)
|
|
47
40
|
@ptr = nil
|
|
48
41
|
end
|
|
49
42
|
end
|
|
@@ -53,12 +46,11 @@ module GRX
|
|
|
53
46
|
end
|
|
54
47
|
|
|
55
48
|
# ------------------------------------------------------------------
|
|
56
|
-
#
|
|
57
|
-
#
|
|
49
|
+
# Read / Write — used in fallback mode and by item/get()
|
|
50
|
+
# High-performance tensor ops operate directly on @ptr in C.
|
|
58
51
|
# ------------------------------------------------------------------
|
|
59
52
|
def read(indice)
|
|
60
53
|
if CAPI::LOADED
|
|
61
|
-
# Leemos 8 bytes desde el offset correcto y los desempaquetamos como double
|
|
62
54
|
@ptr[indice * 8, 8].unpack1("d")
|
|
63
55
|
else
|
|
64
56
|
@data[indice]
|
|
@@ -73,7 +65,7 @@ module GRX
|
|
|
73
65
|
end
|
|
74
66
|
end
|
|
75
67
|
|
|
76
|
-
#
|
|
68
|
+
# Dumps entire buffer to a Ruby Array
|
|
77
69
|
def to_ruby_array
|
|
78
70
|
if CAPI::LOADED
|
|
79
71
|
@ptr[0, @size * 8].unpack("d#{@size}")
|
data/lib/grx/tensor.rb
CHANGED
|
@@ -40,7 +40,7 @@ module GRX
|
|
|
40
40
|
ones(t.shape, requires_grad: requires_grad)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Xavier uniform initialization (optimal for linear layers with tanh/sigmoid)
|
|
44
44
|
def self.xavier_uniform(shape, requires_grad: false)
|
|
45
45
|
fan_in, fan_out = shape[-2] || 1, shape[-1] || 1
|
|
46
46
|
n = shape.reduce(1, :*)
|
|
@@ -49,9 +49,9 @@ module GRX
|
|
|
49
49
|
new(s, shape, requires_grad: requires_grad)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
#
|
|
52
|
+
# He normal initialization (optimal for layers with ReLU)
|
|
53
53
|
def self.he_normal(shape, requires_grad: false)
|
|
54
|
-
# fan_in =
|
|
54
|
+
# fan_in = number of inputs = last dim or penultimate if 2D
|
|
55
55
|
fan_in = shape.size >= 2 ? shape[-1] : shape[0]
|
|
56
56
|
n = shape.reduce(1, :*)
|
|
57
57
|
s = _alloc_raw(n)
|
|
@@ -60,13 +60,13 @@ module GRX
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# ----------------------------------------------------------------
|
|
63
|
-
#
|
|
63
|
+
# ARITHMETIC OPERATIONS (with autograd)
|
|
64
64
|
# ----------------------------------------------------------------
|
|
65
65
|
|
|
66
66
|
def +(other)
|
|
67
67
|
case other
|
|
68
68
|
when Tensor
|
|
69
|
-
raise ShapeError, "
|
|
69
|
+
raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
|
|
70
70
|
r = Tensor.new(_binop(:grx_add, other), @shape)
|
|
71
71
|
if requires_grad || other.requires_grad
|
|
72
72
|
r.requires_grad = true
|
|
@@ -80,14 +80,14 @@ module GRX
|
|
|
80
80
|
when Numeric
|
|
81
81
|
add_scalar(other.to_f)
|
|
82
82
|
else
|
|
83
|
-
raise TypeError, "
|
|
83
|
+
raise TypeError, "Cannot add Tensor with #{other.class}"
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def -(other)
|
|
88
88
|
case other
|
|
89
89
|
when Tensor
|
|
90
|
-
raise ShapeError, "
|
|
90
|
+
raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
|
|
91
91
|
r = Tensor.new(_binop(:grx_sub, other), @shape)
|
|
92
92
|
if requires_grad || other.requires_grad
|
|
93
93
|
r.requires_grad = true
|
|
@@ -101,14 +101,14 @@ module GRX
|
|
|
101
101
|
when Numeric
|
|
102
102
|
add_scalar(-other.to_f)
|
|
103
103
|
else
|
|
104
|
-
raise TypeError, "
|
|
104
|
+
raise TypeError, "Cannot subtract Tensor with #{other.class}"
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def *(other)
|
|
109
109
|
case other
|
|
110
110
|
when Tensor
|
|
111
|
-
raise ShapeError, "
|
|
111
|
+
raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
|
|
112
112
|
r = Tensor.new(_binop(:grx_mul, other), @shape)
|
|
113
113
|
if requires_grad || other.requires_grad
|
|
114
114
|
r.requires_grad = true
|
|
@@ -123,14 +123,14 @@ module GRX
|
|
|
123
123
|
when Numeric
|
|
124
124
|
scale(other.to_f)
|
|
125
125
|
else
|
|
126
|
-
raise TypeError, "
|
|
126
|
+
raise TypeError, "Cannot multiply Tensor with #{other.class}"
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def /(other)
|
|
131
131
|
case other
|
|
132
132
|
when Tensor
|
|
133
|
-
raise ShapeError, "
|
|
133
|
+
raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
|
|
134
134
|
r = Tensor.new(_binop(:grx_div, other), @shape)
|
|
135
135
|
if requires_grad || other.requires_grad
|
|
136
136
|
r.requires_grad = true
|
|
@@ -146,7 +146,7 @@ module GRX
|
|
|
146
146
|
when Numeric
|
|
147
147
|
scale(1.0 / other.to_f)
|
|
148
148
|
else
|
|
149
|
-
raise TypeError, "
|
|
149
|
+
raise TypeError, "Cannot divide Tensor with #{other.class}"
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
@@ -155,23 +155,51 @@ module GRX
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
# ----------------------------------------------------------------
|
|
158
|
-
#
|
|
158
|
+
# SCALAR OPERATIONS
|
|
159
159
|
# ----------------------------------------------------------------
|
|
160
160
|
|
|
161
|
+
def coerce(other)
|
|
162
|
+
case other
|
|
163
|
+
when Numeric
|
|
164
|
+
# Returns reversed [self, other] wrapper to enable 2.0 * tensor
|
|
165
|
+
[Tensor.new(Storage.new(Array.new(numel, other.to_f)), @shape), self]
|
|
166
|
+
else
|
|
167
|
+
raise TypeError, "#{self.class} cannot be coerced with #{other.class}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
161
171
|
def scale(s)
|
|
162
|
-
_unary_c(:grx_scale, s) { |v| v * s }
|
|
172
|
+
r = _unary_c(:grx_scale, s) { |v| v * s }
|
|
173
|
+
if requires_grad
|
|
174
|
+
r.requires_grad = true; r._grafo_hijos << self
|
|
175
|
+
src = self; factor = s.to_f
|
|
176
|
+
r.backward_fn = ->(g) { src.agregar_gradiente(g.scale(factor)) }
|
|
177
|
+
end
|
|
178
|
+
r
|
|
163
179
|
end
|
|
164
180
|
|
|
165
181
|
def add_scalar(s)
|
|
166
|
-
_unary_c(:grx_add_scalar, s) { |v| v + s }
|
|
182
|
+
r = _unary_c(:grx_add_scalar, s) { |v| v + s }
|
|
183
|
+
if requires_grad
|
|
184
|
+
r.requires_grad = true; r._grafo_hijos << self
|
|
185
|
+
src = self
|
|
186
|
+
r.backward_fn = ->(g) { src.agregar_gradiente(g) }
|
|
187
|
+
end
|
|
188
|
+
r
|
|
167
189
|
end
|
|
168
190
|
|
|
169
191
|
def negate
|
|
170
|
-
_unary_c(:grx_negate) { |v| -v }
|
|
192
|
+
r = _unary_c(:grx_negate) { |v| -v }
|
|
193
|
+
if requires_grad
|
|
194
|
+
r.requires_grad = true; r._grafo_hijos << self
|
|
195
|
+
src = self
|
|
196
|
+
r.backward_fn = ->(g) { src.agregar_gradiente(g.negate) }
|
|
197
|
+
end
|
|
198
|
+
r
|
|
171
199
|
end
|
|
172
200
|
|
|
173
201
|
# ----------------------------------------------------------------
|
|
174
|
-
#
|
|
202
|
+
# ELEMENT-WISE MATH (with autograd)
|
|
175
203
|
# ----------------------------------------------------------------
|
|
176
204
|
|
|
177
205
|
def abs
|
|
@@ -249,29 +277,56 @@ module GRX
|
|
|
249
277
|
CAPI.grx_clip(@storage.ptr, lo.to_f, hi.to_f, out.ptr, numel)
|
|
250
278
|
else
|
|
251
279
|
data = to_a.map { |v| v < lo ? lo : (v > hi ? hi : v) }
|
|
252
|
-
return Tensor.create(data, @shape)
|
|
280
|
+
return Tensor.create(data, @shape, requires_grad: @requires_grad)
|
|
253
281
|
end
|
|
254
|
-
Tensor.new(out, @shape)
|
|
282
|
+
r = Tensor.new(out, @shape)
|
|
283
|
+
if @requires_grad
|
|
284
|
+
r.requires_grad = true; r._grafo_hijos << self
|
|
285
|
+
src = self; l = lo.to_f; h = hi.to_f
|
|
286
|
+
r.backward_fn = ->(g) {
|
|
287
|
+
mask = Tensor.create(src.to_a.map { |v| (v >= l && v <= h) ? 1.0 : 0.0 }, src.shape)
|
|
288
|
+
src.agregar_gradiente(g * mask)
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
r
|
|
255
292
|
end
|
|
256
293
|
|
|
257
294
|
# ----------------------------------------------------------------
|
|
258
|
-
#
|
|
295
|
+
# REDUCTIONS (return differentiable scalar Tensor with autograd)
|
|
259
296
|
# ----------------------------------------------------------------
|
|
260
297
|
|
|
261
298
|
def sum
|
|
262
|
-
if CAPI::LOADED
|
|
299
|
+
val = if CAPI::LOADED
|
|
263
300
|
CAPI.grx_sum(@storage.ptr, numel)
|
|
264
301
|
else
|
|
265
302
|
to_a.sum
|
|
266
303
|
end
|
|
304
|
+
r = Tensor.create([val], [1], requires_grad: @requires_grad)
|
|
305
|
+
if @requires_grad
|
|
306
|
+
r._grafo_hijos << self
|
|
307
|
+
src = self
|
|
308
|
+
r.backward_fn = ->(g) {
|
|
309
|
+
src.agregar_gradiente(Tensor.create(Array.new(src.numel, g.item), src.shape))
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
r
|
|
267
313
|
end
|
|
268
314
|
|
|
269
315
|
def mean
|
|
270
|
-
if CAPI::LOADED
|
|
316
|
+
val = if CAPI::LOADED
|
|
271
317
|
CAPI.grx_mean(@storage.ptr, numel)
|
|
272
318
|
else
|
|
273
319
|
to_a.sum.to_f / numel
|
|
274
320
|
end
|
|
321
|
+
r = Tensor.create([val], [1], requires_grad: @requires_grad)
|
|
322
|
+
if @requires_grad
|
|
323
|
+
r._grafo_hijos << self
|
|
324
|
+
src = self; n = numel.to_f
|
|
325
|
+
r.backward_fn = ->(g) {
|
|
326
|
+
src.agregar_gradiente(Tensor.create(Array.new(src.numel, g.item / n), src.shape))
|
|
327
|
+
}
|
|
328
|
+
end
|
|
329
|
+
r
|
|
275
330
|
end
|
|
276
331
|
|
|
277
332
|
def max
|
|
@@ -290,12 +345,40 @@ module GRX
|
|
|
290
345
|
end
|
|
291
346
|
end
|
|
292
347
|
|
|
348
|
+
def argmax
|
|
349
|
+
arr = to_a
|
|
350
|
+
return 0 if arr.empty?
|
|
351
|
+
max_idx = 0
|
|
352
|
+
max_val = arr[0]
|
|
353
|
+
(1...arr.size).each do |i|
|
|
354
|
+
if arr[i] > max_val
|
|
355
|
+
max_val = arr[i]
|
|
356
|
+
max_idx = i
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
max_idx
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def argmin
|
|
363
|
+
arr = to_a
|
|
364
|
+
return 0 if arr.empty?
|
|
365
|
+
min_idx = 0
|
|
366
|
+
min_val = arr[0]
|
|
367
|
+
(1...arr.size).each do |i|
|
|
368
|
+
if arr[i] < min_val
|
|
369
|
+
min_val = arr[i]
|
|
370
|
+
min_idx = i
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
min_idx
|
|
374
|
+
end
|
|
375
|
+
|
|
293
376
|
# ----------------------------------------------------------------
|
|
294
|
-
#
|
|
377
|
+
# LINEAR ALGEBRA
|
|
295
378
|
# ----------------------------------------------------------------
|
|
296
379
|
|
|
297
380
|
def dot(other)
|
|
298
|
-
raise ShapeError, "dot
|
|
381
|
+
raise ShapeError, "dot requires matching shape" if @shape != other.shape
|
|
299
382
|
if CAPI::LOADED
|
|
300
383
|
CAPI.grx_dot(@storage.ptr, other.storage.ptr, numel)
|
|
301
384
|
else
|
|
@@ -304,9 +387,9 @@ module GRX
|
|
|
304
387
|
end
|
|
305
388
|
|
|
306
389
|
def matmul(other)
|
|
307
|
-
raise DimensionError, "matmul
|
|
390
|
+
raise DimensionError, "matmul requires 2D tensors" unless @shape.size == 2 && other.shape.size == 2
|
|
308
391
|
m, k = @shape; k2, n = other.shape
|
|
309
|
-
raise ShapeError, "
|
|
392
|
+
raise ShapeError, "Incompatible dimensions: #{@shape} × #{other.shape}" if k != k2
|
|
310
393
|
out = _alloc_storage(m * n)
|
|
311
394
|
if CAPI::LOADED
|
|
312
395
|
CAPI.grx_matmul(@storage.ptr, other.storage.ptr, out.ptr, m, k, n)
|
|
@@ -323,7 +406,7 @@ module GRX
|
|
|
323
406
|
r._grafo_hijos.push(a, b)
|
|
324
407
|
r.backward_fn = ->(g) {
|
|
325
408
|
# dL/dA = dL/dC × B^T, dL/dB = A^T × dL/dC
|
|
326
|
-
#
|
|
409
|
+
# Uses _matmul_no_grad and _transpose_view to avoid graph recursion
|
|
327
410
|
a.agregar_gradiente(g._matmul_no_grad(b._transpose_view)) if a.requires_grad
|
|
328
411
|
b.agregar_gradiente(a._transpose_view._matmul_no_grad(g)) if b.requires_grad
|
|
329
412
|
}
|
|
@@ -332,7 +415,7 @@ module GRX
|
|
|
332
415
|
end
|
|
333
416
|
|
|
334
417
|
# ----------------------------------------------------------------
|
|
335
|
-
#
|
|
418
|
+
# ACTIVATIONS (with autograd)
|
|
336
419
|
# ----------------------------------------------------------------
|
|
337
420
|
|
|
338
421
|
def relu
|
|
@@ -388,10 +471,39 @@ module GRX
|
|
|
388
471
|
end
|
|
389
472
|
|
|
390
473
|
def softmax
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
474
|
+
dim = @shape[-1]
|
|
475
|
+
batch = numel / dim
|
|
476
|
+
raw = to_a
|
|
477
|
+
out_vals = Array.new(numel)
|
|
478
|
+
|
|
479
|
+
batch.times do |b|
|
|
480
|
+
slice = raw.slice(b * dim, dim)
|
|
481
|
+
max_v = slice.max
|
|
482
|
+
exps = slice.map { |v| Math.exp(v - max_v) }
|
|
483
|
+
sum_e = exps.sum
|
|
484
|
+
dim.times { |j| out_vals[b * dim + j] = exps[j] / sum_e }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
r = Tensor.create(out_vals, @shape, requires_grad: @requires_grad)
|
|
488
|
+
if @requires_grad
|
|
489
|
+
r._grafo_hijos << self
|
|
490
|
+
res = r; src = self
|
|
491
|
+
r.backward_fn = ->(g) {
|
|
492
|
+
s_data = res.to_a
|
|
493
|
+
g_data = g.to_a
|
|
494
|
+
grad_x = Array.new(src.numel, 0.0)
|
|
495
|
+
|
|
496
|
+
batch.times do |b|
|
|
497
|
+
s_row = s_data.slice(b * dim, dim)
|
|
498
|
+
g_row = g_data.slice(b * dim, dim)
|
|
499
|
+
dot = s_row.zip(g_row).sum { |s_val, g_val| s_val * g_val }
|
|
500
|
+
dim.times do |j|
|
|
501
|
+
grad_x[b * dim + j] = s_row[j] * (g_row[j] - dot)
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
src.agregar_gradiente(Tensor.create(grad_x, src.shape))
|
|
506
|
+
}
|
|
395
507
|
end
|
|
396
508
|
r
|
|
397
509
|
end
|
|
@@ -411,7 +523,7 @@ module GRX
|
|
|
411
523
|
agregar_gradiente(grad_inicial)
|
|
412
524
|
end
|
|
413
525
|
|
|
414
|
-
#
|
|
526
|
+
# Topological sorting via iterative post-order DFS (prevents stack overflow on deep graphs)
|
|
415
527
|
orden = []
|
|
416
528
|
visitados = {}
|
|
417
529
|
stack = [[self, false]]
|
|
@@ -428,7 +540,7 @@ module GRX
|
|
|
428
540
|
end
|
|
429
541
|
end
|
|
430
542
|
|
|
431
|
-
#
|
|
543
|
+
# Topological order in post-order: reverse traverses root first down to leaves
|
|
432
544
|
orden.reverse_each do |nodo|
|
|
433
545
|
next unless nodo.grad && nodo.backward_fn
|
|
434
546
|
nodo.backward_fn.call(nodo.grad)
|
|
@@ -447,20 +559,41 @@ module GRX
|
|
|
447
559
|
end
|
|
448
560
|
|
|
449
561
|
# ----------------------------------------------------------------
|
|
450
|
-
#
|
|
562
|
+
# GEOMETRY (zero-copy)
|
|
451
563
|
# ----------------------------------------------------------------
|
|
452
564
|
|
|
453
565
|
def get(*coords)
|
|
454
566
|
@storage.read(_calc_flat_index(coords))
|
|
455
567
|
end
|
|
456
568
|
|
|
569
|
+
def set(*coords, val)
|
|
570
|
+
@storage.write(_calc_flat_index(coords), val.to_f)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def contiguous
|
|
574
|
+
return self if _contiguous?
|
|
575
|
+
c = Tensor.create(to_a, @shape, requires_grad: @requires_grad)
|
|
576
|
+
if @requires_grad
|
|
577
|
+
c._grafo_hijos << self
|
|
578
|
+
src = self
|
|
579
|
+
c.backward_fn = ->(g) { src.agregar_gradiente(g) }
|
|
580
|
+
end
|
|
581
|
+
c
|
|
582
|
+
end
|
|
583
|
+
|
|
457
584
|
def reshape(nueva_forma)
|
|
458
|
-
raise ArgumentError, "
|
|
459
|
-
Tensor.new(@storage, nueva_forma, offset: @offset, requires_grad: @requires_grad)
|
|
585
|
+
raise ArgumentError, "Incompatible reshape" if numel != nueva_forma.reduce(1,:*)
|
|
586
|
+
r = Tensor.new(@storage, nueva_forma, offset: @offset, requires_grad: @requires_grad)
|
|
587
|
+
if @requires_grad
|
|
588
|
+
r._grafo_hijos << self
|
|
589
|
+
src = self; orig_shape = @shape
|
|
590
|
+
r.backward_fn = ->(g) { src.agregar_gradiente(g.reshape(orig_shape)) }
|
|
591
|
+
end
|
|
592
|
+
r
|
|
460
593
|
end
|
|
461
594
|
|
|
462
595
|
def transpose
|
|
463
|
-
raise DimensionError, "transpose
|
|
596
|
+
raise DimensionError, "transpose only supports 2D tensors" if @shape.size != 2
|
|
464
597
|
t = Tensor.new(@storage, [@shape[1], @shape[0]],
|
|
465
598
|
strides: [@strides[1], @strides[0]],
|
|
466
599
|
offset: @offset, requires_grad: @requires_grad)
|
|
@@ -468,32 +601,34 @@ module GRX
|
|
|
468
601
|
t._grafo_hijos << self
|
|
469
602
|
src = self
|
|
470
603
|
t.backward_fn = ->(g) {
|
|
471
|
-
src.agregar_gradiente(g.
|
|
604
|
+
src.agregar_gradiente(g.transpose)
|
|
472
605
|
}
|
|
473
606
|
end
|
|
474
607
|
t
|
|
475
608
|
end
|
|
476
609
|
|
|
477
|
-
# Transpose
|
|
610
|
+
# Transpose view without autograd — for internal backward pass
|
|
478
611
|
def _transpose_view
|
|
479
|
-
raise DimensionError, "transpose
|
|
612
|
+
raise DimensionError, "transpose only supports 2D tensors" if @shape.size != 2
|
|
480
613
|
Tensor.new(@storage, [@shape[1], @shape[0]],
|
|
481
614
|
strides: [@strides[1], @strides[0]],
|
|
482
615
|
offset: @offset, requires_grad: false)
|
|
483
616
|
end
|
|
484
617
|
|
|
485
|
-
# Matmul
|
|
618
|
+
# Matmul without autograd — for internal backward_fn usage
|
|
486
619
|
def _matmul_no_grad(other)
|
|
487
|
-
raise DimensionError, "matmul
|
|
620
|
+
raise DimensionError, "matmul requires 2D tensors" unless @shape.size == 2 && other.shape.size == 2
|
|
488
621
|
m, k = @shape; k2, n = other.shape
|
|
489
|
-
raise ShapeError, "
|
|
622
|
+
raise ShapeError, "Incompatible dimensions" if k != k2
|
|
623
|
+
a_c = _contiguous? ? self : contiguous
|
|
624
|
+
b_c = other._contiguous? ? other : other.contiguous
|
|
490
625
|
out = _alloc_storage(m * n)
|
|
491
626
|
if CAPI::LOADED
|
|
492
|
-
CAPI.grx_matmul(
|
|
627
|
+
CAPI.grx_matmul(a_c.storage.ptr, b_c.storage.ptr, out.ptr, m, k, n)
|
|
493
628
|
else
|
|
494
629
|
result = Array.new(m * n, 0.0)
|
|
495
|
-
m.times { |i| k.times { |kk| aik =
|
|
496
|
-
n.times { |j| result[i*n+j] += aik *
|
|
630
|
+
m.times { |i| k.times { |kk| aik = a_c.storage.read(i*k+kk)
|
|
631
|
+
n.times { |j| result[i*n+j] += aik * b_c.storage.read(kk*n+j) } } }
|
|
497
632
|
return Tensor.new(Storage.new(result), [m, n])
|
|
498
633
|
end
|
|
499
634
|
Tensor.new(out, [m, n])
|
|
@@ -504,16 +639,20 @@ module GRX
|
|
|
504
639
|
end
|
|
505
640
|
|
|
506
641
|
# ----------------------------------------------------------------
|
|
507
|
-
#
|
|
642
|
+
# UTILITIES
|
|
508
643
|
# ----------------------------------------------------------------
|
|
509
644
|
|
|
510
645
|
def numel
|
|
511
646
|
@shape.reduce(1, :*)
|
|
512
647
|
end
|
|
513
648
|
|
|
649
|
+
def rank
|
|
650
|
+
@shape.size
|
|
651
|
+
end
|
|
652
|
+
|
|
514
653
|
def to_a
|
|
515
|
-
#
|
|
516
|
-
#
|
|
654
|
+
# If strides are contiguous (normal tensor, reshape), read buffer directly.
|
|
655
|
+
# Otherwise (transpose, strided views), traverse with custom strides.
|
|
517
656
|
if _contiguous?
|
|
518
657
|
@storage.to_ruby_array
|
|
519
658
|
else
|
|
@@ -521,13 +660,14 @@ module GRX
|
|
|
521
660
|
end
|
|
522
661
|
end
|
|
523
662
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# Un tensor es contiguo si sus strides coinciden con los strides row-major estándar
|
|
527
|
-
def _contiguous?
|
|
663
|
+
# A tensor is contiguous if its strides match standard row-major order
|
|
664
|
+
def contiguous?
|
|
528
665
|
expected = _calc_strides(@shape)
|
|
529
666
|
@strides == expected && @offset == 0
|
|
530
667
|
end
|
|
668
|
+
alias _contiguous? contiguous?
|
|
669
|
+
|
|
670
|
+
private
|
|
531
671
|
|
|
532
672
|
def _collect_elements(shape, strides, offset)
|
|
533
673
|
if shape.size == 1
|
|
@@ -541,18 +681,46 @@ module GRX
|
|
|
541
681
|
|
|
542
682
|
public
|
|
543
683
|
|
|
684
|
+
include Comparable
|
|
685
|
+
|
|
686
|
+
def <=>(other)
|
|
687
|
+
case other
|
|
688
|
+
when Tensor
|
|
689
|
+
(numel == 1 && other.numel == 1) ? item <=> other.item : nil
|
|
690
|
+
when Numeric
|
|
691
|
+
numel == 1 ? item <=> other.to_f : nil
|
|
692
|
+
else
|
|
693
|
+
nil
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
544
697
|
def item
|
|
545
|
-
raise "item()
|
|
698
|
+
raise "item() only supported for 1-element tensors" if numel != 1
|
|
546
699
|
to_a[0]
|
|
547
700
|
end
|
|
548
701
|
|
|
702
|
+
def to_f
|
|
703
|
+
raise "to_f only supported for 1-element tensors" if numel != 1
|
|
704
|
+
to_a[0]
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def to_i
|
|
708
|
+
raise "to_i only supported for 1-element tensors" if numel != 1
|
|
709
|
+
to_a[0].to_i
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def nan?
|
|
713
|
+
raise "nan? only supported for 1-element tensors" if numel != 1
|
|
714
|
+
to_a[0].nan?
|
|
715
|
+
end
|
|
716
|
+
|
|
549
717
|
def to_s
|
|
550
718
|
"#<GRX::Tensor shape=#{@shape} data=#{to_a}>"
|
|
551
719
|
end
|
|
552
720
|
alias inspect to_s
|
|
553
721
|
|
|
554
722
|
# ----------------------------------------------------------------
|
|
555
|
-
#
|
|
723
|
+
# PRIVATE
|
|
556
724
|
# ----------------------------------------------------------------
|
|
557
725
|
|
|
558
726
|
private
|
|
@@ -575,33 +743,34 @@ module GRX
|
|
|
575
743
|
end
|
|
576
744
|
end
|
|
577
745
|
|
|
578
|
-
#
|
|
746
|
+
# Element-wise binary op: delegates to CAPI or Ruby fallback
|
|
579
747
|
def _binop(op, other)
|
|
748
|
+
a_c = _contiguous? ? self : contiguous
|
|
749
|
+
b_c = other._contiguous? ? other : other.contiguous
|
|
580
750
|
out = _alloc_storage(numel)
|
|
581
751
|
if CAPI::LOADED
|
|
582
|
-
CAPI.public_send(op,
|
|
752
|
+
CAPI.public_send(op, a_c.storage.ptr, b_c.storage.ptr, out.ptr, numel)
|
|
583
753
|
else
|
|
584
754
|
rb = { grx_add: :+, grx_sub: :-, grx_mul: :*, grx_div: :/ }[op]
|
|
585
755
|
data = (0...numel).map { |i|
|
|
586
|
-
|
|
756
|
+
a_c.storage.read(a_c.offset + i).public_send(rb, b_c.storage.read(b_c.offset + i))
|
|
587
757
|
}
|
|
588
758
|
return Storage.new(data)
|
|
589
759
|
end
|
|
590
760
|
out
|
|
591
761
|
end
|
|
592
762
|
|
|
593
|
-
#
|
|
594
|
-
# Si el bloque acepta un elemento (arity == 1) → map element-wise
|
|
595
|
-
# Si el bloque no acepta argumentos (arity == 0) → lo llama una vez (para softmax, etc.)
|
|
763
|
+
# Unary op: delegates to CAPI with optional args or Ruby fallback block
|
|
596
764
|
def _unary_c(op, *args, &fallback)
|
|
765
|
+
a_c = _contiguous? ? self : contiguous
|
|
597
766
|
out = _alloc_storage(numel)
|
|
598
767
|
if CAPI::LOADED
|
|
599
|
-
CAPI.public_send(op,
|
|
768
|
+
CAPI.public_send(op, a_c.storage.ptr, *args, out.ptr, numel)
|
|
600
769
|
else
|
|
601
770
|
vals = if fallback
|
|
602
|
-
fallback.arity == 0 ? fallback.call : to_a.map(&fallback)
|
|
771
|
+
fallback.arity == 0 ? fallback.call : a_c.to_a.map(&fallback)
|
|
603
772
|
else
|
|
604
|
-
to_a
|
|
773
|
+
a_c.to_a
|
|
605
774
|
end
|
|
606
775
|
return Tensor.create(vals, @shape)
|
|
607
776
|
end
|