-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrocket_delegate.cpp
More file actions
3660 lines (3492 loc) · 204 KB
/
Copy pathrocket_delegate.cpp
File metadata and controls
3660 lines (3492 loc) · 204 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 The tflite-rocket authors
// ===========================================================================
// File overview and includes
// ===========================================================================
/*
* rocket_delegate.cpp — TFLite external delegate for the RK3588 NPU (rocket).
*
* Runs float32 CONV_2D and DEPTHWISE_CONV_2D on the NPU. General KxK / stride / pad
* (SAME|VALID) / dilation go through the HW-validated rocket_conv2d_fp16; a
* matmul-aligned 1x1 pointwise conv takes a fast path onto the multicore fp16 matmul
* (a 1x1 conv IS a matmul: out[m,cout] = sum_cin in[m,cin]*w[cout,cin], over
* m = H*W positions). Depthwise (depth_multiplier 1 => OC==IC, one KH×KW filter per
* channel) sets desc.depthwise=1 and the driver runs its native depthwise job — the
* SAME pointwise(1x1)+depthwise(KxK) decomposition a MobileNet/SSD backbone is built
* from. Filters are converted + reordered into the driver's NCHW fp16 layout ONCE in
* Prepare; per inference only the activation is transposed in (with the SAME/VALID
* padding materialized) and the result transposed out (+ bias + fused activation).
*
* int8/uint8 CONV_2D has two paths. By DEFAULT the delegate dequantizes at the
* partition boundary (filter once in Prepare, activation per inference), reuses the
* SAME fp16 conv, then requantizes the output — an fp16 approximation of TFLite's
* int8 kernel. With the `native_int8` option a NATIVE int8/uint8 conv path
* runs int8 directly on the NPU with no host dequant (direct + 1x1, plus per-tensor
* symmetric depthwise via the int8-out on-chip-requant runtime); per-channel DW int8
* and uint8 depthwise fall back to the fp16 approximation. See rocket_convert.h.
*
* Beyond conv, the delegate also claims the ops a real detector graph carries AROUND
* its convolutions so the partitioner can take CONTIGUOUS subgraphs instead of one
* partition per conv: ADD (residual), AVERAGE/MAX_POOL_2D, CONCATENATION (SSD head
* joins), and shape-only RESHAPE. These run as thin HOST kernels (float + int8/uint8)
* inside the delegate — memory-bound elementwise/reduction work for which inventing
* NPU regcmd is not worth it in a v1; see rocket_ops.h. They do NOT move work to the
* NPU (and since each conv transposes NHWC<->NCHW itself, they don't save a transpose)
* — the win is fewer/larger partitions, with NCHW-resident inter-op buffers as a
* follow-up. They are gated by the `aux_ops` option (default on) for HW A/B.
*
* Everything this delegate does not claim falls back to CPU via TFLite's graph
* partitioner: depthwise with depth_multiplier != 1, a depthwise layer too big for one
* CBUF pass (the driver has no DW spatial tiling yet), grouped convs, broadcast ADD,
* and fused activations beyond None/Relu/Relu6/ReluN1To1.
*
* Builds to libtflite_rocket.so, loadable by tflite_runtime:
* tflite.load_delegate("libtflite_rocket.so",
* options={"nthreads": "4", "min_macs": "0", "aux_ops": "1", "profile": "0"})
*/
// TFLite C API only (no C++ TFLite lib). These headers ship with Mesa's Teflon build
// (tensorflow/lite/core/c/...); the delegate is a classic C TfLiteDelegate, so the few
// C symbols it references (TfLiteIntArrayCreate/Free) bind at dlopen from the host
// interpreter — exactly like Mesa's libteflon.so. We deliberately do NOT use the C++
// SimpleDelegate helper (its header/lib aren't shipped without a full TFLite build).
#include "tensorflow/lite/core/c/common.h"
#include "tensorflow/lite/core/c/builtin_op_data.h"
#include "tensorflow/lite/builtin_ops.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <memory>
#include <new>
#include <string>
#include <thread>
#include <vector>
#include <climits>
extern "C" {
#include "rocket_npu.h" // rocket_open / rocket_close
#include "rocket_matmul.h" // rocket_ctx / rocket_weights / prepacked matmul (1x1 path)
#include "rocket_conv.h" // rocket_conv2d_fp16 / _plan / _oh / _ow
#include "rocket_pool.h" // rocket_pool_fp16 (opt-in on-NPU PPU MaxPool/AvgPool)
#include "rocket_activation.h"// rocket_activation_fp16 (opt-in on-NPU DPU LUT activation)
#include "rocket_reduce.h" // rocket_global_{avg,max,min}pool_fp16 (opt-in spatial reduce)
#include "rocket_resize.h" // rocket_upsample_nearest_fp16 (opt-in integer-factor resize)
#include "rocket_normvision.h"// rocket_l2norm_fp16 (opt-in on-NPU L2 normalization)
// Pin a worker thread to a big (A76) core. Exported by the driver lib (rocket_affinity.c)
// but not in an installed header; the host-side box-sum/requant fan-out reuses it so the
// threads land on the fast cores (idx % n_big internally; a no-op if pinning is disabled).
void rocket_pin_worker(int worker_idx);
}
#include "rocket_convert.h" // NHWC<->NCHW + SAME/VALID pad + bias/act glue
#include "rocket_ops.h" // host ADD / POOL / CONCAT NHWC kernels (float + quant)
namespace {
// ===========================================================================
// Options and helper utilities (quantization, layout, shape math)
// ===========================================================================
// ---------------------------------------------------------------------------
// options (parsed from the external-delegate key/value pairs)
// ---------------------------------------------------------------------------
struct RocketOptions {
int nthreads = 4; // matmul (1x1) fan-out across the 3 NPU cores (clamped 1..8)
long min_macs = 0; // min OC*OH*OW*IC*KH*KW to offload (0 = every supported conv)
bool aux_ops = true;// claim ADD/POOL/CONCAT/RESHAPE host ops (contiguous partitions)
bool profile = false;
bool native_int8 = false; // run signed-int8 DIRECT/1x1 convs as a NATIVE int8
// conv (int8 x int8 -> int32 on the NPU, host per-axis
// requant) instead of the dequant->fp16->requant approx.
// EXACT int8 accumulate (bit-identical to TFLite's int8
// CPU kernel up to the <=1 requant rounding), no host
// dequant/requant round-trip. OFF by default for an A/B;
// uint8 + depthwise use the fp16 path.
bool mm_int8 = true; // perf Step 1: route native int8/uint8 1x1 DIRECT convs to the
// RESIDENT multicore int8 matmul (rocket_matmul_int8_prepacked)
// instead of the single-core conv pool. A 1x1 IS a matmul; the
// NHWC in/out need no transpose and the integer accumulate is
// exact, so the result is BYTE-IDENTICAL to the conv path (a kill
// switch / A-B toggle — only takes effect under native_int8). K/N
// are zero-padded to %32; M%4||M==1 or it stays on the conv path.
bool act_npu = false; // run the claimed unary activations (HardSwish / Sigmoid) on the
// NPU DPU LUT (rocket_activation_fp16) instead of the exact host
// kernel. OFF by default: the host kernel is exact + free (a
// standalone NPU LUT pass is an extra round-trip), so this is an
// A/B / demonstration toggle for the on-NPU nonlinearity. Falls
// back to the host kernel with no device or on a LUT failure.
bool fc_npu = false; // run a claimed FULLY_CONNECTED on the NPU resident matmul
// instead of the host matmul. OFF: the host matmul beats the
// dispatch-bound NPU for one-shot TFLite FC (not-mac-bound);
// on, for large batched / repeated FC (M%4; M==1 stays host).
bool ew_npu = false; // run a claimed MAXIMUM / MINIMUM on the NPU DPU EW ALU
// (rocket_ew_max/min_fp16) instead of the exact host kernel.
// OFF: like ADD these are memory-bound, so the host kernel is
// exact + free and a standalone NPU EW pass is an extra
// round-trip; on, for A/B / cube-resident fusion. FLOAT only;
// falls back to the host kernel with no device or on failure.
bool norm_npu = false; // run a claimed L2_NORMALIZATION on the NPU (rocket_l2norm_fp16)
// instead of the exact host kernel. OFF: the host kernel is exact
// + free (a standalone NPU norm is an extra round-trip). FLOAT
// only; falls back to host with no device or on failure.
bool resize_npu = false; // run a claimed RESIZE_NEAREST_NEIGHBOR on the NPU
// (rocket_upsample_nearest_fp16) instead of the host kernel.
// OFF: resize is a memory-bound gather (host is exact + free).
// Only an INTEGER-factor, align_corners=false, half_pixel=false
// nearest resize (floor mode = the NPU's block replication) is
// routed — anything else (bilinear, non-integer, other modes)
// stays on the exact host kernel. Falls back to host on failure.
bool pool_npu = false; // run a claimed MAX_POOL_2D / AVERAGE_POOL_2D on the NPU PPU
// (rocket_pool_fp16) instead of the exact host kernel. OFF: the
// host kernel is exact + free (a standalone PPU pool is a 2nd
// round-trip + NHWC<->cube transpose until partitions stay
// cube-resident). FLOAT only; AVERAGE only when VALID (the PPU
// divides by KH*KW = count-include-pad, vs TFLite's valid count,
// so a padded average diverges — MAX with pad is fine). Falls
// back to the host kernel with no device or on a PPU failure.
bool nchw_resident = false; // keep conv->conv intermediates in fp16-NCHW (skips the
// per-boundary transpose + int8 requant/dequant). Big win
// (single block 1.37x), but OFF by default: the skipped int8
// requant makes the activation diverge from the int8 reference,
// UNBOUNDEDLY when a resident value reaches an output predictor
// undamped (SSD/stack max|delegate-CPU| 2 -> 65). Needs a
// re-quantization-barrier rule (or mAP validation) before it can
// default on. nchw_resident=1 opts in (validated on chains that
// re-quantize before the output, e.g. a MobileNetV2 block).
};
// A 1x1 stride-1 conv we can hand to the matmul: just the matmul's HARD alignment
// (K%32, N%16, M%4||M==1; M = H*W positions, K = IC, N = OC). The old "worth
// offloading" size floor (K,N>=64, M>=4) is gone: with the weights resident (packed
// once in Prepare, only the activation packed per call) even a small aligned 1x1 is
// cheaper on the matmul than re-running the 5-BO conv job, so every aligned 1x1
// offloads. Non-aligned 1x1s (e.g. IC%32!=0) still take the conv path.
static bool dims_offloadable(int M, int K, int Ncout) {
return (K % 32 == 0) && (Ncout % 16 == 0) && (M % 4 == 0 || M == 1) && M >= 1;
}
static int round_up(int x, int m) { return (x + m - 1) / m * m; }
static int map_activation(TfLiteFusedActivation a) {
switch (a) {
case kTfLiteActNone: return ROCKET_ACT_NONE;
case kTfLiteActRelu: return ROCKET_ACT_RELU;
case kTfLiteActRelu6: return ROCKET_ACT_RELU6;
case kTfLiteActReluN1To1: return ROCKET_ACT_RELUN1;
default: return -1; // tanh/sigmoid/etc. -> leave the node on CPU
}
}
static bool is_quant_type(TfLiteType t) { return t == kTfLiteInt8 || t == kTfLiteUInt8; }
// A byte-preserving layout op (reshape/transpose/slice/pad) moves quantized bytes
// unchanged, so it is only correct when input and output carry the SAME affine quant
// params. Reject mismatched (or differently-typed) quant metadata so a malformed graph
// falls back to the CPU instead of silently emitting numerically-wrong bytes. Float
// tensors have no quant params -> trivially equal.
static bool same_quant_params(const TfLiteTensor &a, const TfLiteTensor &b) {
if (a.type != b.type) return false;
if (!is_quant_type(a.type)) return true;
return a.params.scale == b.params.scale && a.params.zero_point == b.params.zero_point;
}
// Bytes per element for the data-path types the delegate handles (the layout ops are
// byte-exact over any of them); 0 for an unsupported type (so the op declines to CPU).
// Matches RESHAPE: float32 and int8/uint8 only — int32 etc. stay on the CPU.
static int type_elem_size(TfLiteType t) {
switch (t) {
case kTfLiteFloat32: return 4;
case kTfLiteInt8: case kTfLiteUInt8: return 1;
default: return 0;
}
}
// Read element i of a constant int32/int64 index tensor (TFLite begin/size/axis come
// as either). Returns 0 for an unsupported type — callers validate the type first.
static long read_const_int(const TfLiteTensor &t, int i) {
if (t.type == kTfLiteInt32) return (long)t.data.i32[i];
if (t.type == kTfLiteInt64) return (long)t.data.i64[i];
return 0;
}
// Map a delegate unary-activation kind (ROCKET_UNARY_*) to the driver's on-NPU DPU
// LUT kind (ROCKET_ACTIVATION_*); -1 if no LUT path exists. Used only by the opt-in
// act_npu route.
static int unary_to_rocket_act(int kind) {
switch (kind) {
case ROCKET_UNARY_HARDSWISH: return ROCKET_ACTIVATION_HARDSWISH;
case ROCKET_UNARY_SIGMOID: return ROCKET_ACTIVATION_SIGMOID;
case ROCKET_UNARY_HARDSIGMOID: return ROCKET_ACTIVATION_HARDSIGMOID;
case ROCKET_UNARY_TANH: return ROCKET_ACTIVATION_TANH;
// ELU has a dedicated driver entry point (rocket_elu_fp16, with the x~=0
// signed-output host repair), NOT the generic rocket_activation_fp16 LUT call;
// eval_act special-cases it. Return its enum so callers can name it.
case ROCKET_UNARY_ELU: return ROCKET_ACTIVATION_ELU;
// LOG rides the generic LUT call, but the LUT is domain-limited ([~0.25,32]); the
// host kernel (exact logf) is the default, act_npu is the approximate demo route.
case ROCKET_UNARY_LOG: return ROCKET_ACTIVATION_LOG;
// LEAKY_RELU has a dedicated parametric driver entry point (rocket_leaky_relu_fp16,
// per-tensor alpha); eval_act special-cases it like ELU.
case ROCKET_UNARY_LEAKY_RELU: return ROCKET_ACTIVATION_LEAKY_RELU;
// The positive-/symmetric-domain LUT kinds. Domain-limited (like LOG), so the exact
// host kernel is the default and act_npu is the approximate demo route.
case ROCKET_UNARY_EXP: return ROCKET_ACTIVATION_EXP;
case ROCKET_UNARY_SQRT: return ROCKET_ACTIVATION_SQRT;
case ROCKET_UNARY_RSQRT: return ROCKET_ACTIVATION_RSQRT;
case ROCKET_UNARY_ABS: return ROCKET_ACTIVATION_ABS;
// RELU/RELU6/RELU_N1_1/NEG/SQUARE/FLOOR are exact host arithmetic with no LUT
// (RELU is max(x,0), not a curve); host-only, no NPU route.
default: return -1;
}
}
// Product of tensor extents with overflow + negative-extent rejection. Returns the
// element count, or -1 if any extent is negative or the running product overflows.
// Model-supplied dims are untrusted: a negative extent or a wrapping product would
// otherwise size an allocation to a near-SIZE_MAX or small-then-overrun buffer.
static long long safe_elem_count(const TfLiteIntArray *d) {
if (!d) return -1;
long long n = 1;
for (int i = 0; i < d->size; i++) {
const long long e = d->data[i];
if (e < 0) return -1;
if (e != 0 && n > LLONG_MAX / e) return -1; // multiply would overflow
n *= e;
}
return n;
}
// Total element count of a dims array (product of extents; batch included). Returns
// -1 on a negative/overflowing product so downstream sizing/index math fails the
// validation checks instead of wrapping.
static long dims_count(const TfLiteIntArray *d) {
const long long n = safe_elem_count(d);
return (n < 0 || n > LONG_MAX) ? -1 : (long)n;
}
// Identical shape (rank + every extent) — the no-broadcast elementwise gate.
static bool same_dims(const TfLiteIntArray *a, const TfLiteIntArray *b) {
if (!a || !b || a->size != b->size) return false;
for (int i = 0; i < a->size; i++) if (a->data[i] != b->data[i]) return false;
return true;
}
// True if `in` broadcasts to `out` (NumPy / TFLite right-aligned: in_rank <= out_rank,
// and aligning the trailing axes, each in dim is 1 or == the out dim). The broadcast
// elementwise gate — analyze_add/binary/arith claim a node when BOTH inputs broadcast to
// the output shape; eval_add then materializes each to the output shape (rocket_broadcast_copy)
// and runs the existing same-shape host kernel, so the result is bit-exact.
static bool broadcast_to(const TfLiteIntArray *in, const TfLiteIntArray *out) {
if (!in || !out || in->size > out->size) return false;
int off = out->size - in->size;
for (int i = 0; i < in->size; i++)
if (in->data[i] != 1 && in->data[i] != out->data[off + i]) return false;
return true;
}
// Identical shape ignoring one axis (rank + every non-axis extent) — the concat gate:
// inputs may differ only along the concatenation axis, and the copy applies the
// output-derived outer/inner strides to each input, so the non-axis extents must match.
static bool same_dims_except(const TfLiteIntArray *a, const TfLiteIntArray *b, int axis) {
if (!a || !b || a->size != b->size) return false;
for (int i = 0; i < a->size; i++)
if (i != axis && a->data[i] != b->data[i]) return false;
return true;
}
// ===========================================================================
// Node support and graph partitioning
//
// The claimed-op structs (ConvNode / FCNode / the aux-op params), the per-op
// analyze_* gates that decide which nodes the delegate claims and capture their
// static parameters, and analyze_node, the single dispatch the partitioner and
// the kernel's Init both call.
// ===========================================================================
// One claimed CONV_2D node. Filter-derived dims (IC/OC/KH/KW) + stride/dilation/act
// are static and captured at Init; the spatial dims and padding are recomputed per
// Eval from the live tensors (so a correct result survives an input resize, even if
// the matmul-vs-conv routing was decided for the Init-time shape).
struct ConvNode {
int in_idx, w_idx, bias_idx, out_idx;
int IC, OC, KH, KW;
int sy, sx, dy, dx;
int act; // ROCKET_ACT_*
bool depthwise = false; // DEPTHWISE_CONV_2D (OC==IC, one KH×KW filter/channel)
std::vector<_Float16> w; // direct [OC][IC][KH][KW] / depthwise [C][KH][KW] fp16
bool packed = false;
// resident matmul (1x1 pointwise) path: the fp16 weight w == the matmul B[OC,IC],
// packed ONCE into NPU BOs in Prepare and kept resident on the kernel's mm_ctx_, so
// Eval only packs the activation per inference (vs rocket_matmul_fp16_mt opening fds
// + re-scattering B every call). mm_w is null when the conv is not matmul-aligned, the
// device/scratch-cache is unavailable, or the input M changed (resize) -> conv path.
rocket_weights *mm_w = nullptr;
int mm_M = 0; // spatial M (=IH*IW) the resident weights were packed for
// resident int8 matmul (native int8/uint8 1x1 pointwise) path: the recentered/raw int8
// weight is zero-padded to [Np,Kp] (Np=ceil(OC,32), Kp=ceil(IC,32)) and scattered into
// resident NPU BOs ONCE on the kernel's mm_i8_ctx_; Eval then runs the int8 matmul
// (output columns fanned across the 3 cores) + a per-pixel requant. mm_w_i8 is null when
// the conv is not native int8/uint8, M%4||M==1 fails, mm_int8 is off, or no device.
rocket_i8_weights *mm_w_i8 = nullptr;
int mm_i8_M = 0; // spatial M the resident int8 weights were packed for
// quantized (int8/uint8) path — dequant at the boundary, reuse the fp16 conv
bool is_quant = false;
int in_uns = 0, w_uns = 0, out_uns = 0; // 1 = uint8 storage for that tensor
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
std::vector<float> w_scale; // [OC] per-axis filter scale
std::vector<int> w_zp; // [OC] per-axis filter zero point
std::vector<float> bias_f; // [OC] dequantized bias (empty when no bias)
// NATIVE int8 path (native_int8 option). Set for a signed-int8 DIRECT/1x1 conv
// with symmetric weights: int8 x int8 -> int32 on the NPU + host per-axis requant
// (EXACT int8, no fp16 approximation). Mutually exclusive with the dequant->fp16
// path; uint8 / depthwise / w_zp!=0 keep is_quant on this struct but native=false.
bool native = false; // direct: int32-raw + per-axis requant
bool native_dw = false; // depthwise: int8-out on-chip requant (per-tensor)
bool native_u8 = false; // direct UINT8 (Option D): recenter to int8 + box-sum requant
bool needs_boxsum = false; // native_u8 with an asymmetric weight zp (some w_zp != 128)
std::vector<int8_t> w_i8; // direct [OC][IC][KH][KW] / dw [C][KH][KW] raw int8
std::vector<int32_t> eff_bias; // direct: [OC] = bias_q - in_zp*Σ_kernel w_q
std::vector<int32_t> bias_q; // dw: raw TFLite int32 bias [C] (runtime folds the corr)
bool has_bias_q = false; // a real TFLite int32 bias is present
};
// One claimed FULLY_CONNECTED node (float). A matmul C[M,N] = A[M,K] * B[N,K]^T + bias,
// fused act: M = flattened batch (input elems / K), K = input_dim (weights.dims[1]),
// N = units (weights.dims[0]). The weight B[N,K] is exactly the 1x1-conv / matmul B layout,
// so it packs ONCE into resident NPU BOs (mm_w on the kernel's mm_ctx_) and Eval runs the
// resident fp16 matmul (rocket_matmul_fp16_prepacked) — the same path the 1x1 pointwise conv
// uses. K is zero-padded to %32 and N to %16 (Kpad/Npad) so ANY FC head offloads (the pad
// zeros are transparent to the dot product); a host matmul (rocket_fc_f) is the fd<0 / no-pack
// fallback. M must be 1 or %4 (the matmul alignment); other M (rare) -> CPU fallback.
struct FCNode {
int in_idx, w_idx, bias_idx, out_idx;
int M, K, N; // batch / input_dim / units (logical, unpadded)
int act; // ROCKET_ACT_*
// weight/bias are model constants -> read straight from the live tensors (w.data.f /
// bias.data.f) in Prepare (pack) + Eval (host fallback); no stored copy.
rocket_weights *mm_w = nullptr; // resident padded [Npad][Kpad] weight (null => host path)
int mm_M = 0; // the M the resident weight was packed for (resize guard)
};
// Pull per-axis (or per-tensor) filter quant params into ws/wz (length OC).
// Returns false if the params are missing or an unexpected length.
static bool fill_filter_quant(const TfLiteTensor &flt, int OC,
std::vector<float> &ws, std::vector<int> &wz) {
ws.assign(OC, 0.f);
wz.assign(OC, 0);
if (flt.quantization.type == kTfLiteAffineQuantization && flt.quantization.params) {
const auto *aq =
reinterpret_cast<const TfLiteAffineQuantization *>(flt.quantization.params);
if (!aq->scale || aq->scale->size < 1) return false;
const int ss = aq->scale->size;
if (ss != 1 && ss != OC) return false; // not per-tensor or per-OC
const int zs = aq->zero_point ? aq->zero_point->size : 0;
if (zs != 0 && zs != 1 && zs != OC) return false; // reject mismatched zero_point len
for (int oc = 0; oc < OC; oc++) {
ws[oc] = aq->scale->data[ss == 1 ? 0 : oc];
wz[oc] = zs ? aq->zero_point->data[zs == 1 ? 0 : oc] : 0;
}
return true;
}
// legacy per-tensor params
if (flt.params.scale == 0.f) return false;
for (int oc = 0; oc < OC; oc++) { ws[oc] = flt.params.scale; wz[oc] = flt.params.zero_point; }
return true;
}
// Gate + parameter extraction shared by IsNodeSupportedByDelegate and the kernel's
// Init. Handles CONV_2D and DEPTHWISE_CONV_2D (depth_multiplier==1 => OC==IC, one
// KH×KW filter per channel; the driver runs it as a native depthwise job). Returns
// true iff this is a conv the delegate can run; fills *out (when non-null) with the
// static parameters.
static bool analyze_conv(const TfLiteRegistration *reg, const TfLiteNode *node,
TfLiteContext *ctx, const RocketOptions &opts, ConvNode *out)
{
const bool is_dw = (reg->builtin_code == kTfLiteBuiltinDepthwiseConv2d);
if (reg->builtin_code != kTfLiteBuiltinConv2d && !is_dw) return false;
// CONV_2D and DEPTHWISE_CONV_2D are distinct builtins with distinct param structs;
// both carry the same padding/stride/dilation/activation. Depthwise adds
// depth_multiplier (only 1 is supported => OC==IC).
int act, sy, sx, dy, dx;
if (is_dw) {
const auto *p = reinterpret_cast<const TfLiteDepthwiseConvParams *>(node->builtin_data);
if (!p) return false;
if (p->depth_multiplier != 1) return false; // OC==IC only; else CPU fallback
act = map_activation(p->activation);
sy = p->stride_height; sx = p->stride_width;
dy = p->dilation_height_factor; dx = p->dilation_width_factor;
} else {
const auto *p = reinterpret_cast<const TfLiteConvParams *>(node->builtin_data);
if (!p) return false;
act = map_activation(p->activation);
sy = p->stride_height; sx = p->stride_width;
dy = p->dilation_height_factor; dx = p->dilation_width_factor;
}
if (act < 0) return false;
if (sy < 1 || sx < 1) return false;
if (dy < 1 || dx < 1) return false;
if (node->inputs->size < 2 || node->outputs->size < 1) return false;
const TfLiteTensor &in = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &flt = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &outp = ctx->tensors[node->outputs->data[0]];
const bool has_bias = node->inputs->size > 2 && node->inputs->data[2] >= 0;
auto is_q = [](TfLiteType t) { return t == kTfLiteInt8 || t == kTfLiteUInt8; };
const bool float_path = (in.type == kTfLiteFloat32 && flt.type == kTfLiteFloat32 &&
outp.type == kTfLiteFloat32);
const bool quant_path = is_q(in.type) && is_q(flt.type) && is_q(outp.type);
if (!float_path && !quant_path) return false; // float or int8/uint8
if (has_bias) {
const TfLiteType bt = ctx->tensors[node->inputs->data[2]].type;
if (float_path && bt != kTfLiteFloat32) return false;
if (quant_path && bt != kTfLiteInt32) return false; // int8 conv bias = int32
}
if (!in.dims || in.dims->size != 4 || !flt.dims || flt.dims->size != 4 ||
!outp.dims || outp.dims->size != 4) return false;
if (in.dims->data[0] != 1) return false; // batch 1 only
const int IH = in.dims->data[1], IW = in.dims->data[2], IC = in.dims->data[3];
// Filter layout differs: conv [OC][KH][KW][IC]; depthwise [1][KH][KW][C] (OC==IC==C).
int OC, KH, KW, FIC;
if (is_dw) {
if (flt.dims->data[0] != 1) return false; // depth_multiplier folded into C
KH = flt.dims->data[1]; KW = flt.dims->data[2]; FIC = flt.dims->data[3];
OC = IC;
} else {
OC = flt.dims->data[0]; KH = flt.dims->data[1]; KW = flt.dims->data[2];
FIC = flt.dims->data[3];
}
const int OH = outp.dims->data[1], OW = outp.dims->data[2];
if (FIC != IC) return false; // grouped conv (filter C != input C)
if (outp.dims->data[3] != OC) return false;
// OC%16!=0 is fine: the driver pads OC up to the weight oc group (16) and slices the
// result. IC%G + single-CBUF-pass fit (incl. DW channel/spatial tiling) are gated by
// rocket_conv2d_plan on the materialized descriptor below.
if (OH <= 0 || OW <= 0) return false;
// worth-offloading floor. Depthwise reduces only KH*KW per output (no IC sum).
const long macs = is_dw ? (long)OC * OH * OW * KH * KW
: (long)OC * OH * OW * IC * KH * KW;
if (macs < opts.min_macs) return false;
// matmul-offloadable 1x1 stride-1 pointwise? (direct float only — depthwise does a
// per-channel reduction, never a matmul; the quant path always runs the conv so it
// can dequant/requant at the boundary)
const int M = IH * IW;
const bool mm_ok = !is_dw && float_path
&& (KH == 1 && KW == 1 && sy == 1 && sx == 1 && dy == 1 && dx == 1
&& dims_offloadable(M, IC, OC));
// general conv: validate the MATERIALIZED descriptor (the SAME/VALID pad is
// folded into a larger zero-haloed input, so the driver runs pad_top=pad_left=0)
// through the driver's pure planner, and confirm it reproduces TFLite's dims. For
// depthwise this is also the IC%G + single-CBUF-pass gate (a DW layer too big for
// one pass returns <0 -> CPU fallback, since the driver has no DW spatial tiling).
const int tot_h = rocket_total_pad(IH, KH, sy, dy, OH);
const int tot_w = rocket_total_pad(IW, KW, sx, dx, OW);
rocket_conv2d_desc d = {};
d.ic = IC; d.ih = IH + tot_h; d.iw = IW + tot_w; d.oc = OC;
d.kh = KH; d.kw = KW; d.stride_y = sy; d.stride_x = sx;
d.pad_top = 0; d.pad_left = 0; d.dil_y = dy; d.dil_x = dx; d.depthwise = is_dw ? 1 : 0;
const bool conv_ok = (rocket_conv2d_plan(&d) == 0)
&& rocket_conv2d_oh(&d) == OH && rocket_conv2d_ow(&d) == OW;
if (!mm_ok && !conv_ok) return false;
// Validate (and, for quant, capture) the quantization params. Done even when
// out==null so IsNodeSupportedByDelegate never claims a node Init can't run.
// Depthwise filter quant is per-channel along axis 3 (the C dim); since OC==C and
// channel c maps to output channel c, the flat per-OC array fill_filter_quant
// produces (length OC) indexes correctly for either layout.
std::vector<float> ws;
std::vector<int> wz;
if (quant_path) {
if (in.params.scale <= 0.f || outp.params.scale <= 0.f) return false;
if (!fill_filter_quant(flt, OC, ws, wz)) return false;
}
if (out) {
out->in_idx = node->inputs->data[0];
out->w_idx = node->inputs->data[1];
out->bias_idx = has_bias ? node->inputs->data[2] : -1;
out->out_idx = node->outputs->data[0];
out->IC = IC; out->OC = OC; out->KH = KH; out->KW = KW;
out->sy = sy; out->sx = sx; out->dy = dy; out->dx = dx;
out->act = act;
out->depthwise = is_dw;
out->is_quant = quant_path;
if (quant_path) {
out->in_uns = (in.type == kTfLiteUInt8);
out->w_uns = (flt.type == kTfLiteUInt8);
out->out_uns = (outp.type == kTfLiteUInt8);
out->in_scale = in.params.scale; out->in_zp = in.params.zero_point;
out->out_scale = outp.params.scale; out->out_zp = outp.params.zero_point;
// NATIVE int8: signed-int8 DIRECT/1x1 with symmetric weights only.
// A quant conv always takes the conv path (mm_ok is float-only), and conv_ok
// is already required above, so the int8 runtime can run it (1x1 = degenerate
// conv). uint8 / depthwise / per-axis w_zp!=0 stay on the dequant->fp16 path.
bool all_w_sym = true;
for (int oc = 0; oc < OC; oc++) if (wz[oc] != 0) { all_w_sym = false; break; }
const bool all_i8 = in.type == kTfLiteInt8 && flt.type == kTfLiteInt8 &&
outp.type == kTfLiteInt8;
const bool all_u8 = in.type == kTfLiteUInt8 && flt.type == kTfLiteUInt8 &&
outp.type == kTfLiteUInt8;
// The int8 input zero-point is materialized into the conv pad halo via an
// int8 memset; an out-of-int8-range in_zp would truncate there. Likewise the
// uint8 path recenters in_zp by -128 into the int8 range. Reject anything
// outside the type's range so it stays on the dequant->fp16 path instead.
const bool in_zp_i8_ok = in.params.zero_point >= -128 && in.params.zero_point <= 127;
const bool in_zp_u8_ok = in.params.zero_point >= 0 && in.params.zero_point <= 255;
// DIRECT native: int32-raw + host per-axis requant (per-axis weights are fine).
out->native = opts.native_int8 && !is_dw && all_i8 && all_w_sym && in_zp_i8_ok;
// DIRECT native UINT8 (Option D): the common coral/MediaPipe case (uint8 I/O,
// asymmetric per-tensor weight zp). Recenter the uint8 operands to int8 on the
// host (x=in_q-128, y=w_q-128 — always in range), reuse the SAME int32-raw NPU
// conv, and fold the centering into eff_bias + a per-output-pixel box-sum
// correction (the price of w_zp != 128). No symmetry requirement; uint8
// depthwise stays on fp16 (a follow-on). Mutually exclusive with native (i8 vs u8).
out->native_u8 = opts.native_int8 && !is_dw && all_u8 && in_zp_u8_ok;
if (out->native_u8) {
// box-sum is only needed when some weight zp != 128 (else beta == 0); the
// real MobileDet weights are all asymmetric, so this is normally true.
bool sym128 = true;
for (int oc = 0; oc < OC; oc++) if (wz[oc] != 128) { sym128 = false; break; }
out->needs_boxsum = !sym128;
}
// DEPTHWISE native (int8-out on-chip requant): PER-TENSOR quant only (Teflon's
// constraint; per-channel DW needs BS_MUL, stays on fp16) and a SYMMETRIC pad
// (the runtime uses the CNA's symmetric HW pad — the validated config; an
// asymmetric SAME pad falls back to fp16). w_zp==0 (symmetric weights).
bool w_per_tensor = false;
if (flt.quantization.type == kTfLiteAffineQuantization && flt.quantization.params) {
const auto *aq =
reinterpret_cast<const TfLiteAffineQuantization *>(flt.quantization.params);
w_per_tensor = aq->scale && aq->scale->size == 1;
} else if (flt.params.scale != 0.f) {
w_per_tensor = true; // legacy per-tensor
}
out->native_dw = opts.native_int8 && is_dw && all_i8 && all_w_sym &&
w_per_tensor && (tot_h % 2 == 0) && (tot_w % 2 == 0) && in_zp_i8_ok;
out->has_bias_q = has_bias;
out->w_scale = std::move(ws);
out->w_zp = std::move(wz);
}
}
return true;
}
// ---------------------------------------------------------------------------
// Aux host ops: the elementwise / pooling / join ops around the convs. Each is a
// thin NHWC host kernel (rocket_ops.h); claiming them keeps the delegated partition
// contiguous. Quant params here are PER-TENSOR (input/output scale+zp), unlike the
// conv filter's per-axis params. Only static params are captured here; spatial dims
// are re-read from live tensors in Eval (resize-safe, like the conv path).
// ---------------------------------------------------------------------------
enum class NodeKind { Conv, Add, Pool, Concat, Reshape, Act, FC, Prelu, Reduce, Resize, TConv,
L2Norm, LogSoftmax, Softmax, Cumsum, Transpose, Pad, Slice, Split };
// A standalone unary activation node (HARD_SWISH / LOGISTIC): pure elementwise,
// in/out the SAME shape. kind is a ROCKET_UNARY_* code (rocket_ops.h). Quant is
// per-tensor (one scale/zp each side), like the other aux ops.
struct ActP {
int in, out, kind;
float param = 0.f; // LEAKY_RELU slope (alpha); ignored by the other kinds
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
};
struct AddP {
int in0, in1, out, act;
// op selects the binary kernel: -1 = ADD (rocket_add, fused act), else a
// ROCKET_BINOP_* (MAXIMUM / MINIMUM, no fused act). Keeps one NodeKind/struct
// for all same-shape two-tensor elementwise ops.
int op = -1;
bool is_quant = false;
int in0_uns = 0, in1_uns = 0, out_uns = 0;
float in0_scale = 1.f, in1_scale = 1.f, out_scale = 1.f;
int in0_zp = 0, in1_zp = 0, out_zp = 0;
};
struct PoolP {
int in, out;
int kh, kw, sy, sx, same, is_avg, act;
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
};
struct ConcatP {
std::vector<int> ins;
int out, axis, act;
bool is_quant = false;
int out_uns = 0; float out_scale = 1.f; int out_zp = 0;
std::vector<int> in_uns;
std::vector<float> in_scale;
std::vector<int> in_zp;
};
struct ReshapeP { int in, out, elem_size; }; // shape-only: byte copy
// LAYOUT OPS (TRANSPOSE / PAD / SLICE / SPLIT): pure byte moves — values pass
// through unchanged and the quant scale/zp are identical in==out, so the dtype is
// just elem_size (1 int8/uint8, 4 float32). perm/paddings/begin/size/axis are model
// constants captured at analyze; the live shapes are re-read + re-validated at eval
// (the external delegate can't un-delegate at Invoke). The NPU has no on-chip
// layout-conversion engine, so all four are exact host kernels (rocket_ops.h); the
// value is keeping a real graph's conv->layout->conv in ONE delegated partition.
struct TransposeP { int in, out, elem_size, rank; int perm[ROCKET_MAX_RANK]; };
// SLICE / STRIDED_SLICE: gather begin[d]..begin[d]+size[d] (stride 1) per INPUT axis.
// size[] is stored explicitly (not read from the output dims) so STRIDED_SLICE's
// shrink_axis_mask — which drops a size-1 axis, making the output rank < input rank —
// still works: the gather runs in input rank, the output holds the same elements in
// the same order, validated by an element-count match.
struct SliceP { int in, out, elem_size, rank; int begin[ROCKET_MAX_RANK]; int size[ROCKET_MAX_RANK];
int stride[ROCKET_MAX_RANK]; bool strided = false; }; // strided=false => stride[] unused (SLICE/SPLIT)
struct PadP {
int in, out, elem_size, rank;
int pad_before[ROCKET_MAX_RANK];
bool is_quant = false;
float pad_value_f = 0.f; // float PAD (0) / PADV2 constant
signed char pad_byte = 0; // quant fill byte (zero_point, or PADV2's quantized const)
};
// SPLIT / SPLIT_V: N outputs, each a contiguous slice along `axis`. The per-output
// begin[axis] is the running sum of the earlier outputs' axis extents (computed at
// eval from the live output dims). Multi-output (the only such NodeKind).
struct SplitP { int in, elem_size, rank, axis; std::vector<int> outs; };
// PRELU: per-channel parametric ReLU. alpha is a model constant (alpha_idx),
// dequantized to float[C] at eval. x/out per-tensor quant like the other aux ops.
struct PreluP {
int in, alpha_idx, out, C;
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
bool alpha_uns = false;
float alpha_scale = 1.f;
int alpha_zp = 0;
};
// SPATIAL REDUCE (MEAN / REDUCE_MAX / REDUCE_MIN over axes [1,2]): in [1,H,W,C] ->
// out [1,C] or [1,1,1,C]. op = ROCKET_REDUCE_*. x/out per-tensor quant.
struct ReduceP {
int in, out, H, W, C, op;
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
};
// RESIZE_NEAREST_NEIGHBOR / RESIZE_BILINEAR: in [1,IH,IW,C] -> [1,OH,OW,C]. bilinear
// selects op; align_corners / half_pixel from the op params. Quant params unchanged
// (resize never rescales), so quant carries one (scale,zp) for in==out.
struct ResizeP {
int in, out, IH, IW, C, OH, OW;
bool bilinear = false, align_corners = false, half_pixel = false;
bool is_quant = false;
int uns = 0, elem_size = 4;
float scale = 1.f; int zp = 0;
};
// TRANSPOSE_CONV (float v1): learned upsample. weights TFLite [OC,KH,KW,IC] (const),
// optional bias (const). pad_h/pad_w precomputed the TFLite way. host exact kernel.
struct TConvP {
int in, w_idx, bias_idx, out;
int IH, IW, IC, OC, KH, KW, sy, sx, pad_h, pad_w, OH, OW, act;
};
// L2_NORMALIZATION: per-spatial-position normalize over the channel (last) axis.
// M = product of all dims except last; C = last dim. x/out per-tensor quant.
struct L2NormP {
int in, out, M, C;
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
};
// LOG_SOFTMAX / CUMSUM: over the last axis. M = product of all dims except last,
// N = last. Cumsum carries exclusive/reverse. x/out per-tensor quant.
struct SeqP {
int in, out, M, N;
int exclusive = 0, reverse = 0; // cumsum only
float beta = 1.f; // softmax only (logit scale; TFLite default 1)
bool is_quant = false;
int in_uns = 0, out_uns = 0;
float in_scale = 1.f, out_scale = 1.f;
int in_zp = 0, out_zp = 0;
};
// A claimed node: exactly one of the embedded params is live per `kind` (the unused
// ones stay default — cheap; partitions are small).
struct Node {
NodeKind kind = NodeKind::Conv;
ConvNode conv;
AddP add;
PoolP pool;
ConcatP concat;
ReshapeP reshape{};
ActP unary;
FCNode fc;
PreluP prelu;
ReduceP reduce;
ResizeP resize;
TConvP tconv;
L2NormP l2norm;
SeqP seq;
TransposeP transpose;
PadP pad;
SliceP slice;
SplitP split;
};
// ADD: elementwise residual, SAME shape only (broadcast -> CPU). float | int8 | uint8.
static bool analyze_add(const TfLiteNode *node, TfLiteContext *ctx, Node *out) {
const auto *p = reinterpret_cast<const TfLiteAddParams *>(node->builtin_data);
if (!p) return false;
const int act = map_activation(p->activation);
if (act < 0) return false;
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &a = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &b = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!broadcast_to(a.dims, o.dims) || !broadcast_to(b.dims, o.dims)) return false; // a,b broadcast to o
const bool fl = a.type == kTfLiteFloat32 && b.type == kTfLiteFloat32 && o.type == kTfLiteFloat32;
const bool q = is_quant_type(a.type) && a.type == b.type && a.type == o.type;
if (!fl && !q) return false;
if (q && (a.params.scale <= 0.f || b.params.scale <= 0.f || o.params.scale <= 0.f)) return false;
if (out) {
out->kind = NodeKind::Add;
AddP &x = out->add;
x.in0 = node->inputs->data[0]; x.in1 = node->inputs->data[1];
x.out = node->outputs->data[0]; x.act = act; x.is_quant = q;
if (q) {
x.in0_uns = (a.type == kTfLiteUInt8); x.in1_uns = (b.type == kTfLiteUInt8);
x.out_uns = (o.type == kTfLiteUInt8);
x.in0_scale = a.params.scale; x.in0_zp = a.params.zero_point;
x.in1_scale = b.params.scale; x.in1_zp = b.params.zero_point;
x.out_scale = o.params.scale; x.out_zp = o.params.zero_point;
}
}
return true;
}
// MAXIMUM / MINIMUM: elementwise two-tensor, SAME shape only (broadcast -> CPU).
// No fused-activation param (the op is the whole node). float | int8 | uint8.
// Shares NodeKind::Add / AddP, distinguished by AddP.op (ROCKET_BINOP_MAX/MIN).
static bool analyze_binary(const TfLiteRegistration *reg, const TfLiteNode *node,
TfLiteContext *ctx, Node *out) {
int op;
switch (reg->builtin_code) {
case kTfLiteBuiltinMaximum: op = ROCKET_BINOP_MAX; break;
case kTfLiteBuiltinMinimum: op = ROCKET_BINOP_MIN; break;
default: return false;
}
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &a = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &b = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!broadcast_to(a.dims, o.dims) || !broadcast_to(b.dims, o.dims)) return false; // a,b broadcast to o
const bool fl = a.type == kTfLiteFloat32 && b.type == kTfLiteFloat32 && o.type == kTfLiteFloat32;
const bool q = is_quant_type(a.type) && a.type == b.type && a.type == o.type;
if (!fl && !q) return false;
if (q && (a.params.scale <= 0.f || b.params.scale <= 0.f || o.params.scale <= 0.f)) return false;
if (out) {
out->kind = NodeKind::Add;
AddP &x = out->add;
x.in0 = node->inputs->data[0]; x.in1 = node->inputs->data[1];
x.out = node->outputs->data[0]; x.act = ROCKET_ACT_NONE; x.op = op; x.is_quant = q;
if (q) {
x.in0_uns = (a.type == kTfLiteUInt8); x.in1_uns = (b.type == kTfLiteUInt8);
x.out_uns = (o.type == kTfLiteUInt8);
x.in0_scale = a.params.scale; x.in0_zp = a.params.zero_point;
x.in1_scale = b.params.scale; x.in1_zp = b.params.zero_point;
x.out_scale = o.params.scale; x.out_zp = o.params.zero_point;
}
}
return true;
}
// MUL / SUB / DIV: elementwise two-tensor, SAME shape only (broadcast -> CPU), WITH a
// fused activation (like ADD). float | int8 | uint8. Shares NodeKind::Add / AddP,
// distinguished by AddP.op (ROCKET_ARITH_MUL/SUB/DIV); eval_add routes these to the
// fused-act arithmetic host kernel. SUB/DIV are non-commutative: in0 op in1.
static bool analyze_arith(const TfLiteRegistration *reg, const TfLiteNode *node,
TfLiteContext *ctx, Node *out) {
int op, act;
switch (reg->builtin_code) {
case kTfLiteBuiltinMul: {
const auto *p = reinterpret_cast<const TfLiteMulParams *>(node->builtin_data);
if (!p) return false; op = ROCKET_ARITH_MUL; act = map_activation(p->activation); break; }
case kTfLiteBuiltinSub: {
const auto *p = reinterpret_cast<const TfLiteSubParams *>(node->builtin_data);
if (!p) return false; op = ROCKET_ARITH_SUB; act = map_activation(p->activation); break; }
case kTfLiteBuiltinDiv: {
const auto *p = reinterpret_cast<const TfLiteDivParams *>(node->builtin_data);
if (!p) return false; op = ROCKET_ARITH_DIV; act = map_activation(p->activation); break; }
default: return false;
}
if (act < 0) return false;
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &a = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &b = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!broadcast_to(a.dims, o.dims) || !broadcast_to(b.dims, o.dims)) return false; // a,b broadcast to o
const bool fl = a.type == kTfLiteFloat32 && b.type == kTfLiteFloat32 && o.type == kTfLiteFloat32;
const bool q = is_quant_type(a.type) && a.type == b.type && a.type == o.type;
if (!fl && !q) return false;
if (q && (a.params.scale <= 0.f || b.params.scale <= 0.f || o.params.scale <= 0.f)) return false;
if (out) {
out->kind = NodeKind::Add;
AddP &x = out->add;
x.in0 = node->inputs->data[0]; x.in1 = node->inputs->data[1];
x.out = node->outputs->data[0]; x.act = act; x.op = op; x.is_quant = q;
if (q) {
x.in0_uns = (a.type == kTfLiteUInt8); x.in1_uns = (b.type == kTfLiteUInt8);
x.out_uns = (o.type == kTfLiteUInt8);
x.in0_scale = a.params.scale; x.in0_zp = a.params.zero_point;
x.in1_scale = b.params.scale; x.in1_zp = b.params.zero_point;
x.out_scale = o.params.scale; x.out_zp = o.params.zero_point;
}
}
return true;
}
// PRELU: per-channel parametric ReLU, out = x>=0 ? x : alpha[c]*x. inputs[0]=x,
// inputs[1]=alpha (a model CONSTANT, one slope per channel). Claimed when alpha is a
// constant whose element count == the channel (last) dim of x and broadcasts over the
// other axes (the standard Keras shared_axes=[1,2] / detector PReLU). Same shape/type
// in->out. float | int8 | uint8 (per-tensor x/out/alpha quant). The channel is NHWC's
// innermost index, so the host kernel needs only C (= last dim).
static bool analyze_prelu(const TfLiteNode *node, TfLiteContext *ctx, Node *out) {
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &in = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &al = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!same_dims(in.dims, o.dims)) return false;
if (!in.dims || in.dims->size < 1) return false;
if (!al.data.data) return false; // alpha must be a constant
const int C = in.dims->data[in.dims->size - 1];
if (C <= 0 || dims_count(al.dims) != (long)C) return false; // per-channel, broadcast
const bool fl = in.type == kTfLiteFloat32 && o.type == kTfLiteFloat32 && al.type == kTfLiteFloat32;
const bool q = is_quant_type(in.type) && in.type == o.type && is_quant_type(al.type);
if (!fl && !q) return false;
if (q && (in.params.scale <= 0.f || o.params.scale <= 0.f || al.params.scale <= 0.f)) return false;
if (out) {
out->kind = NodeKind::Prelu;
PreluP &x = out->prelu;
x.in = node->inputs->data[0]; x.alpha_idx = node->inputs->data[1];
x.out = node->outputs->data[0]; x.C = C; x.is_quant = q;
if (q) {
x.in_uns = (in.type == kTfLiteUInt8); x.out_uns = (o.type == kTfLiteUInt8);
x.alpha_uns = (al.type == kTfLiteUInt8);
x.in_scale = in.params.scale; x.in_zp = in.params.zero_point;
x.out_scale = o.params.scale; x.out_zp = o.params.zero_point;
x.alpha_scale = al.params.scale; x.alpha_zp = al.params.zero_point;
}
}
return true;
}
// MEAN / REDUCE_MAX / REDUCE_MIN over the SPATIAL axes [1,2]: in [1,H,W,C] ->
// [1,C] (keepdims=False) or [1,1,1,C] (True). inputs[1] is the axis CONSTANT, which
// must be exactly the set {1,2}. batch 1. float | int8 | uint8 (per-tensor quant).
// Other axis sets / ranks / reduce ops -> CPU (this is the global-pool form only).
static bool analyze_reduce(const TfLiteRegistration *reg, const TfLiteNode *node,
TfLiteContext *ctx, Node *out) {
int op;
switch (reg->builtin_code) {
case kTfLiteBuiltinMean: op = ROCKET_REDUCE_MEAN; break;
case kTfLiteBuiltinReduceMax: op = ROCKET_REDUCE_MAX; break;
case kTfLiteBuiltinReduceMin: op = ROCKET_REDUCE_MIN; break;
default: return false;
}
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &in = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &ax = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!in.dims || in.dims->size != 4 || in.dims->data[0] != 1) return false; // NHWC, batch 1
if (!ax.data.data || ax.type != kTfLiteInt32) return false; // axis constant
if (dims_count(ax.dims) != 2) return false; // exactly 2 axes
// accept {1,2} in either order (normalize negatives: -3->1, -2->2 for rank 4)
int a0 = ax.data.i32[0], a1 = ax.data.i32[1];
auto norm = [](int a) { return a < 0 ? a + 4 : a; };
a0 = norm(a0); a1 = norm(a1);
if (!((a0 == 1 && a1 == 2) || (a0 == 2 && a1 == 1))) return false; // spatial only
const int H = in.dims->data[1], W = in.dims->data[2], C = in.dims->data[3];
if (H <= 0 || W <= 0 || C <= 0) return false;
if (dims_count(o.dims) != (long)C) return false; // [1,C] or [1,1,1,C]
const bool fl = in.type == kTfLiteFloat32 && o.type == kTfLiteFloat32;
const bool q = is_quant_type(in.type) && in.type == o.type;
if (!fl && !q) return false;
if (q && (in.params.scale <= 0.f || o.params.scale <= 0.f)) return false;
if (out) {
out->kind = NodeKind::Reduce;
ReduceP &x = out->reduce;
x.in = node->inputs->data[0]; x.out = node->outputs->data[0];
x.H = H; x.W = W; x.C = C; x.op = op; x.is_quant = q;
if (q) {
x.in_uns = (in.type == kTfLiteUInt8); x.out_uns = (o.type == kTfLiteUInt8);
x.in_scale = in.params.scale; x.in_zp = in.params.zero_point;
x.out_scale = o.params.scale; x.out_zp = o.params.zero_point;
}
}
return true;
}
// RESIZE_NEAREST_NEIGHBOR / RESIZE_BILINEAR: in [1,IH,IW,C] -> [1,OH,OW,C].
// inputs[1] is the int32 size CONSTANT {OH,OW}. align_corners/half_pixel come from
// the op params; the host kernel mirrors TFLite's coordinate math exactly. Quant:
// resize never rescales, so in/out must carry the SAME (scale,zp); nearest is a byte
// gather, bilinear a dequant->lerp->requant. float | int8 | uint8, batch 1.
static bool analyze_resize(const TfLiteRegistration *reg, const TfLiteNode *node,
TfLiteContext *ctx, Node *out) {
bool bilinear;
bool align_corners, half_pixel;
switch (reg->builtin_code) {
case kTfLiteBuiltinResizeNearestNeighbor: {
bilinear = false;
const auto *p = reinterpret_cast<const TfLiteResizeNearestNeighborParams *>(node->builtin_data);
if (!p) return false;
align_corners = p->align_corners; half_pixel = p->half_pixel_centers;
break;
}
case kTfLiteBuiltinResizeBilinear: {
bilinear = true;
const auto *p = reinterpret_cast<const TfLiteResizeBilinearParams *>(node->builtin_data);
if (!p) return false;
align_corners = p->align_corners; half_pixel = p->half_pixel_centers;
break;
}
default: return false;
}
if (node->inputs->size != 2 || node->outputs->size < 1) return false;
const TfLiteTensor &in = ctx->tensors[node->inputs->data[0]];
const TfLiteTensor &sz = ctx->tensors[node->inputs->data[1]];
const TfLiteTensor &o = ctx->tensors[node->outputs->data[0]];
if (!in.dims || in.dims->size != 4 || in.dims->data[0] != 1) return false;
if (!o.dims || o.dims->size != 4) return false;
if (!sz.data.data || sz.type != kTfLiteInt32 || dims_count(sz.dims) != 2) return false;
const int OH = sz.data.i32[0], OW = sz.data.i32[1];
const int IH = in.dims->data[1], IW = in.dims->data[2], C = in.dims->data[3];
if (OH <= 0 || OW <= 0 || IH <= 0 || IW <= 0 || C <= 0) return false;
if (o.dims->data[1] != OH || o.dims->data[2] != OW || o.dims->data[3] != C) return false;
const bool fl = in.type == kTfLiteFloat32 && o.type == kTfLiteFloat32;
const bool q = is_quant_type(in.type) && in.type == o.type;
if (!fl && !q) return false;
if (q) {
// resize keeps quant params; require in==out so nearest can byte-gather safely.
if (in.params.scale <= 0.f) return false;
if (in.params.scale != o.params.scale || in.params.zero_point != o.params.zero_point)
return false;
}
if (out) {
out->kind = NodeKind::Resize;
ResizeP &x = out->resize;
x.in = node->inputs->data[0]; x.out = node->outputs->data[0];
x.IH = IH; x.IW = IW; x.C = C; x.OH = OH; x.OW = OW;
x.bilinear = bilinear; x.align_corners = align_corners; x.half_pixel = half_pixel;
x.is_quant = q;
if (q) {
x.uns = (in.type == kTfLiteUInt8); x.elem_size = 1;
x.scale = in.params.scale; x.zp = in.params.zero_point;
}
}
return true;
}