|
55 | 55 |
|
56 | 56 | class Encode(nn.Module): |
57 | 57 | def forward(self, img): |
58 | | - eo = m._encoder(img) |
59 | | - eo = tt._input_filter(eo.permute(0, 3, 1, 2)).permute(0, 2, 3, 1) |
| 58 | + eo_raw = m._encoder(img) # [1,28,28,512] — also feeds the bbox decoder |
| 59 | + eo = tt._input_filter(eo_raw.permute(0, 3, 1, 2)).permute(0, 2, 3, 1) |
60 | 60 | ei = eo.reshape(1, -1, eo.size(-1)).permute(1, 0, 2) |
61 | 61 | pos = ei.shape[0] |
62 | | - return tt._encoder(ei, mask=torch.zeros((nh, pos, pos), dtype=torch.bool)) |
| 62 | + mem = tt._encoder(ei, mask=torch.zeros((nh, pos, pos), dtype=torch.bool)) |
| 63 | + return mem, eo_raw |
| 64 | + |
| 65 | + |
| 66 | +class BBoxDecode(nn.Module): |
| 67 | + # docling's BBoxDecoder.inference, batched over cells: each cell's tag hidden |
| 68 | + # state attends over the (bbox-decoder-filtered) encoder output to a box. |
| 69 | + def forward(self, enc_out, tag_h): # enc_out [1,28,28,512], tag_h [N,512] |
| 70 | + bd = m._bbox_decoder |
| 71 | + e = bd._input_filter(enc_out.permute(0, 3, 1, 2)).permute(0, 2, 3, 1) |
| 72 | + e = e.reshape(1, -1, e.size(3)) # [1, 784, 512] |
| 73 | + n = tag_h.shape[0] |
| 74 | + h = bd._init_h(e.mean(dim=1)).expand(n, -1) # [N, dim] (same for all cells) |
| 75 | + a = bd._attention |
| 76 | + att = a._full_att( |
| 77 | + a._relu( |
| 78 | + a._encoder_att(e) |
| 79 | + + a._tag_decoder_att(tag_h).unsqueeze(1) |
| 80 | + + a._language_att(h).unsqueeze(1) |
| 81 | + ) |
| 82 | + ).squeeze(2) |
| 83 | + alpha = a._softmax(att) # [N, 784] |
| 84 | + awe = (e * alpha.unsqueeze(2)).sum(dim=1) # [N, 512] |
| 85 | + h = (bd._sigmoid(bd._f_beta(h)) * awe) * h |
| 86 | + return bd._bbox_embed(h).sigmoid(), bd._class_embed(h) |
63 | 87 |
|
64 | 88 |
|
65 | 89 | class Decode(nn.Module): |
@@ -88,39 +112,57 @@ def check(name, a, b): |
88 | 112 | return d |
89 | 113 |
|
90 | 114 |
|
| 115 | +from torch.export import Dim # noqa: E402 |
| 116 | + |
91 | 117 | img = torch.randn(1, 3, 448, 448) |
92 | 118 | with torch.no_grad(): |
93 | | - mem = Encode()(img) |
| 119 | + mem, enc_out = Encode()(img) |
94 | 120 | torch.onnx.export( |
95 | 121 | Encode(), (img,), f"{OUT}/encoder.onnx", |
96 | | - input_names=["image"], output_names=["memory"], opset_version=17, dynamo=False, |
| 122 | + input_names=["image"], output_names=["memory", "enc_out"], |
| 123 | + opset_version=17, dynamo=False, |
97 | 124 | ) |
98 | 125 | tags = torch.full((4, 1), start, dtype=torch.long) |
99 | 126 | with torch.no_grad(): |
100 | 127 | logits, hidden = Decode()(tags, mem) |
101 | 128 | # The dynamo exporter is needed here: the legacy tracer bakes the sequence length |
102 | 129 | # into nn.MultiheadAttention's reshape, so a 1-token first step fails. dynamo keeps |
103 | 130 | # the `seq` axis symbolic. |
104 | | -from torch.export import Dim # noqa: E402 |
105 | | - |
106 | 131 | seq = Dim("seq", min=1, max=1024) |
107 | 132 | torch.onnx.export( |
108 | 133 | Decode(), (tags, mem), f"{OUT}/decoder.onnx", |
109 | 134 | input_names=["tags", "memory"], output_names=["logits", "hidden"], |
110 | 135 | dynamo=True, dynamic_shapes=({0: seq}, {}), |
111 | 136 | ) |
| 137 | +# bbox decoder: N cell hiddens → N boxes (+ classes). N is dynamic. |
| 138 | +tag_h = torch.randn(5, 512) |
| 139 | +with torch.no_grad(): |
| 140 | + boxes, classes = BBoxDecode()(enc_out, tag_h) |
| 141 | +ncells = Dim("ncells", min=1, max=1024) |
| 142 | +torch.onnx.export( |
| 143 | + BBoxDecode(), (enc_out, tag_h), f"{OUT}/bbox.onnx", |
| 144 | + input_names=["enc_out", "tag_h"], output_names=["boxes", "classes"], |
| 145 | + dynamo=True, dynamic_shapes=({}, {0: ncells}), |
| 146 | +) |
112 | 147 |
|
113 | 148 | import onnxruntime as ort # noqa: E402 |
114 | 149 |
|
115 | 150 | print("encoder.onnx:") |
116 | | -eo = ort.InferenceSession(f"{OUT}/encoder.onnx").run(None, {"image": img.numpy()})[0] |
117 | | -check("memory", eo, mem.numpy()) |
| 151 | +eres = ort.InferenceSession(f"{OUT}/encoder.onnx").run(None, {"image": img.numpy()}) |
| 152 | +check("memory", eres[0], mem.numpy()) |
| 153 | +check("enc_out", eres[1], enc_out.numpy()) |
118 | 154 | print("decoder.onnx:") |
119 | 155 | do = ort.InferenceSession(f"{OUT}/decoder.onnx").run( |
120 | 156 | None, {"tags": tags.numpy(), "memory": mem.numpy()} |
121 | 157 | ) |
122 | 158 | check("logits", do[0], logits.numpy()) |
123 | 159 | check("hidden", do[1], hidden.numpy()) |
| 160 | +print("bbox.onnx:") |
| 161 | +bo = ort.InferenceSession(f"{OUT}/bbox.onnx").run( |
| 162 | + None, {"enc_out": enc_out.numpy(), "tag_h": tag_h.numpy()} |
| 163 | +) |
| 164 | +check("boxes", bo[0], boxes.numpy()) |
| 165 | +check("classes", bo[1], classes.numpy()) |
124 | 166 |
|
125 | 167 | # word map → tokens file for the Rust decode loop |
126 | 168 | json.dump( |
|
0 commit comments