mlboydaisuke commited on
Commit
b7a4afe
Β·
verified Β·
1 Parent(s): f6a798a

Document the rank-3 attention GPU miscompute and the v2 fix

Browse files
Files changed (1) hide show
  1. README.md +22 -13
README.md CHANGED
@@ -34,16 +34,25 @@ turns a **point prompt** into segmentation masks **per tap** (a few ms each) β€”
34
  | Inputs | `image_embeddings [1,256,64,64]`, `sparse_prompt [1,2,256]`, `feat_s1 [1,64,128,128]`, `feat_s0 [1,32,256,256]` |
35
  | Outputs | `pred_masks [1,3,256,256]` (logits, 3 multimask candidates), `iou_scores [1,3]` |
36
  | Precision / size | FP16, **17 MB** |
37
- | Device | Pixel 8a β€” GPU-resident (358/358 LITERT_CL) but run on **CPU** for correct masks (see below) |
 
38
  | Op set | `banned ops = NONE`, `>4-D tensors = 0` (BATCH_MATMUL Γ—15, SOFTMAX Γ—7, GELU Γ—2, CONV_2D Γ—2) |
39
 
40
- > **⚠ Residency β‰  correctness.** This decoder fully delegates to the LiteRT GPU (358/358 LITERT_CL
41
- > nodes), but on the Pixel 8a its **GPU fp16 output is numerically wrong** β€” a face tap that the CPU
42
- > decoder segments at IoU β‰ˆ 0.62 collapses to β‰ˆ 0.10 with the mask on the background under the GPU
43
- > delegate. The companion encoder's GPU output is fine (encoder-GPU + decoder-CPU matches all-CPU
44
- > exactly). It is **not** LayerNorm (plain vs. overflow-safe give the same wrong GPU result); the
45
- > offending op is still being localized with a per-op GPU dump. **Run this decoder on CPU** (it is tiny
46
- > and fast); the heavy image encoder is the part that benefits from the GPU.
 
 
 
 
 
 
 
 
47
 
48
  ## Pipeline (how the inputs are produced)
49
 
@@ -112,9 +121,9 @@ card). Mask boundaries are carried by the near-exact high-resolution features, s
112
  // once per image - encoder on GPU (decoder-ready v2 variant from the companion repo)
113
  val enc = CompiledModel.create(context.assets, "sam2_tiny_image_encoder_v2_fp16.tflite",
114
  CompiledModel.Options(Accelerator.GPU), null)
115
- // per tap - decoder on CPU (GPU-resident but fp16-incorrect on device; see the note above)
116
- val dec = CompiledModel.create(context.assets, "sam2_tiny_mask_decoder_fp16.tflite",
117
- CompiledModel.Options(Accelerator.CPU), null)
118
  // dec inputs: 0 image_embeddings[1,256,64,64], 1 sparse[1,2,256],
119
  // 2 feat_s1[1,64,128,128], 3 feat_s0[1,32,256,256]
120
  // dec outputs: pred_masks[1,3,256,256] logits, iou_scores[1,3] -> argmax(iou), threshold 0
@@ -143,8 +152,8 @@ coord = 2 * np.pi * ((2 * (np.array([px, py], np.float32) + 0.5) / 1024 - 1) @ p
143
  tok0 = np.concatenate([np.sin(coord), np.cos(coord)]) + pe1
144
  sparse = np.stack([tok0, nap])[None].astype(np.float32) # [1,2,256]
145
 
146
- # 3) decode masks (CPU)
147
- dec = Interpreter(model_path="sam2_tiny_mask_decoder_fp16.tflite"); dec.allocate_tensors()
148
  feed = {(1,2,256): sparse}; feed.update(eo) # match inputs by shape
149
  for d in dec.get_input_details(): dec.set_tensor(d["index"], feed[tuple(d["shape"])])
150
  dec.invoke()
 
34
  | Inputs | `image_embeddings [1,256,64,64]`, `sparse_prompt [1,2,256]`, `feat_s1 [1,64,128,128]`, `feat_s0 [1,32,256,256]` |
35
  | Outputs | `pred_masks [1,3,256,256]` (logits, 3 multimask candidates), `iou_scores [1,3]` |
36
  | Precision / size | FP16, **17 MB** |
37
+ | File | **`sam2_tiny_mask_decoder_v2_fp16.tflite`** (recommended). `sam2_tiny_mask_decoder_fp16.tflite` is the earlier build, kept for reference β€” see the note below. |
38
+ | Device | Pixel 8a β€” **fully GPU** (LITERT_CL), correct masks, ~7 ms/tap |
39
  | Op set | `banned ops = NONE`, `>4-D tensors = 0` (BATCH_MATMUL Γ—15, SOFTMAX Γ—7, GELU Γ—2, CONV_2D Γ—2) |
40
 
41
+ > **⚠ Residency β‰  correctness β€” and why v2 exists.** The first build (`sam2_tiny_mask_decoder_fp16.tflite`)
42
+ > fully delegated to the GPU (358/358 LITERT_CL nodes, `banned ops = NONE`, `>4-D = 0`, desktop parity
43
+ > corr 1.0) yet returned **silently wrong masks on the Pixel 8a GPU** (corr 0.265 vs CPU; a face tap at
44
+ > IoU β‰ˆ 0.62 on CPU collapsed to β‰ˆ 0.10 with the mask on the background).
45
+ >
46
+ > The cause was found by device A/B bisection: its attention was written with the **batch dim collapsed**
47
+ > (q/k/v shaped `[heads, N, d]`, rank 3). The GPU delegate mis-computes that form. It is **not** an fp16
48
+ > problem (forcing fp32 GPU compute still gives corr 0.473) and **not** LayerNorm (plain and
49
+ > overflow-safe LN give the same wrong result). The mask head's rank-2 matmul is innocent.
50
+ >
51
+ > **`v2` keeps the leading batch dim (rank-4 SDPA, `[1, heads, N, d]`).** Host numerics are identical
52
+ > (eager cos 0.999999); on the Pixel 8a GPU it restores **corr 0.9998 / binary-IoU 0.999** vs CPU and is
53
+ > **~20 % faster** (6.8 ms vs 8.5 ms). Inputs and outputs are unchanged, so v2 is a drop-in replacement.
54
+ > Note the companion encoder's rank-3 SDPA *is* GPU-correct β€” a healthy sibling graph proves nothing;
55
+ > only a numeric GPU-vs-CPU check on device catches this.
56
 
57
  ## Pipeline (how the inputs are produced)
58
 
 
121
  // once per image - encoder on GPU (decoder-ready v2 variant from the companion repo)
122
  val enc = CompiledModel.create(context.assets, "sam2_tiny_image_encoder_v2_fp16.tflite",
123
  CompiledModel.Options(Accelerator.GPU), null)
124
+ // per tap - decoder on GPU (v2: rank-4 attention, GPU-correct; see the note above)
125
+ val dec = CompiledModel.create(context.assets, "sam2_tiny_mask_decoder_v2_fp16.tflite",
126
+ CompiledModel.Options(Accelerator.GPU), null)
127
  // dec inputs: 0 image_embeddings[1,256,64,64], 1 sparse[1,2,256],
128
  // 2 feat_s1[1,64,128,128], 3 feat_s0[1,32,256,256]
129
  // dec outputs: pred_masks[1,3,256,256] logits, iou_scores[1,3] -> argmax(iou), threshold 0
 
152
  tok0 = np.concatenate([np.sin(coord), np.cos(coord)]) + pe1
153
  sparse = np.stack([tok0, nap])[None].astype(np.float32) # [1,2,256]
154
 
155
+ # 3) decode masks
156
+ dec = Interpreter(model_path="sam2_tiny_mask_decoder_v2_fp16.tflite"); dec.allocate_tensors()
157
  feed = {(1,2,256): sparse}; feed.update(eo) # match inputs by shape
158
  for d in dec.get_input_details(): dec.set_tensor(d["index"], feed[tuple(d["shape"])])
159
  dec.invoke()