Tarul commited on
Commit
f21a310
·
verified ·
1 Parent(s): 96b8dd1

Upload pxg_tiny/runtime_pipeline.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. pxg_tiny/runtime_pipeline.py +231 -0
pxg_tiny/runtime_pipeline.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PXG-Tiny offline inference runtime — pure NumPy, no torch.
2
+
3
+ Loads an INT8 weight bundle (gen_int8.npz + vq_int8.npz + runtime.json) and
4
+ generates 16x16 sprite grids from English captions with a KV-cached causal
5
+ decoder. Weights are stored as per-output-channel INT8 (weight-only
6
+ quantization, `key.q`/`key.s` int8/float32 pairs) and dequantized once at
7
+ load; all compute is float32. The intent encoder mirrors the torch
8
+ TransformerEncoderLayer (norm_first, ReLU FFN); decoder blocks use
9
+ pre-LN + tanh-GELU exactly as trained.
10
+ """
11
+ import json
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+
16
+ SQ2PI = float(np.sqrt(2.0 / np.pi))
17
+
18
+
19
+ def layer_norm(x, w, b, eps=1e-5):
20
+ m = x.mean(axis=-1, keepdims=True)
21
+ v = x.var(axis=-1, keepdims=True)
22
+ return (x - m) / np.sqrt(v + eps) * w + b
23
+
24
+
25
+ def tanh_gelu(x):
26
+ return 0.5 * x * (1.0 + np.tanh(SQ2PI * (x + 0.044715 * x ** 3)))
27
+
28
+
29
+ def softmax(x, axis=-1):
30
+ x = x - x.max(axis=axis, keepdims=True)
31
+ e = np.exp(x)
32
+ return e / e.sum(axis=axis, keepdims=True)
33
+
34
+
35
+ def dequant(z, name):
36
+ """name.q int8 (out,in) + name.s scales -> dequantized (W, b);
37
+ falls back to plain fp32 `name.w` when the layer was exported unquantized
38
+ (intent encoder stays fp32 for prompt-understanding fidelity)."""
39
+ if name + ".q" in z:
40
+ w = z[name + ".q"].astype(np.float32) * z[name + ".s"].astype(
41
+ np.float32)[:, None]
42
+ else:
43
+ w = z[name + ".w"].astype(np.float32)
44
+ return w, z[name + ".b"].astype(np.float32)
45
+
46
+
47
+ class OfflinePipeline:
48
+ def __init__(self, bundle_dir):
49
+ bdir = Path(bundle_dir)
50
+ cfg = json.loads((bdir / "runtime.json").read_text())
51
+ self.d = cfg["d_model"]
52
+ self.L = cfg["n_layers"]
53
+ self.H = cfg["n_heads"]
54
+ self.dh = self.d // self.H
55
+ self.prefix_len = cfg["prefix_len"]
56
+ self.seq_vis = cfg["seq_vis"]
57
+ self.caps_len = cfg["caps_len"]
58
+ self.temperature = cfg["sampling"]["temperature"]
59
+ self.top_k = cfg["sampling"]["top_k"]
60
+
61
+ z = np.load(bdir / "gen_int8.npz")
62
+ self.char_emb = z["char_emb"].astype(np.float32)
63
+ self.vis_emb = z["vis_emb"].astype(np.float32)
64
+ self.pos_emb = z["pos_emb"].astype(np.float32)
65
+ self.head_w = z["head.w"].astype(np.float32)
66
+ self.head_b = z["head.b"].astype(np.float32)
67
+ self.lnf = (z["lnf.w"].astype(np.float32), z["lnf.b"].astype(np.float32))
68
+
69
+ # intent encoder (bidirectional, ReLU FFN, norm-first)
70
+ self.enc_attn = dequant(z, "enc.attn")
71
+ self.enc_attn_out = dequant(z, "enc.attn_out")
72
+ self.enc_ff1 = dequant(z, "enc.ff1")
73
+ self.enc_ff2 = dequant(z, "enc.ff2")
74
+ self.enc_n1 = (z["enc.n1.w"].astype(np.float32), z["enc.n1.b"].astype(np.float32))
75
+ self.enc_n2 = (z["enc.n2.w"].astype(np.float32), z["enc.n2.b"].astype(np.float32))
76
+ self.enc_proj = dequant(z, "enc.proj")
77
+ self.enc_ln = (z["enc.ln.w"].astype(np.float32), z["enc.ln.b"].astype(np.float32))
78
+
79
+ self.blocks = []
80
+ for i in range(self.L):
81
+ self.blocks.append({
82
+ "ln1": (z[f"b{i}.ln1.w"].astype(np.float32),
83
+ z[f"b{i}.ln1.b"].astype(np.float32)),
84
+ "qkv": dequant(z, f"b{i}.qkv"),
85
+ "proj": dequant(z, f"b{i}.proj"),
86
+ "ln2": (z[f"b{i}.ln2.w"].astype(np.float32),
87
+ z[f"b{i}.ln2.b"].astype(np.float32)),
88
+ "fc1": dequant(z, f"b{i}.fc1"),
89
+ "fc2": dequant(z, f"b{i}.fc2"),
90
+ })
91
+
92
+ vq = np.load(bdir / "vq_int8.npz")
93
+ self.palette = vq["palette"].astype(np.uint8) # (32,4) RGBA
94
+
95
+ # ------------------------------------------------------------ encoder --
96
+ def encode_text(self, ids):
97
+ """ids (32,) int -> prefix (8, d). Mirrors the torch intent encoder:
98
+ char emb -> TransformerEncoderLayer(norm_first, relu) -> chunk-mean ->
99
+ Linear -> tanh-GELU -> LN."""
100
+ T = self.caps_len
101
+ x = self.char_emb[ids]
102
+ qkv = self._lin(layer_norm(x, *self.enc_n1), self.enc_attn)
103
+ q, k, v = np.split(qkv, 3, axis=-1)
104
+ q = q.reshape(T, self.H, self.dh).transpose(1, 0, 2)
105
+ k = k.reshape(T, self.H, self.dh).transpose(1, 0, 2)
106
+ v = v.reshape(T, self.H, self.dh).transpose(1, 0, 2)
107
+ att = softmax(q @ k.transpose(0, 2, 1) / np.sqrt(self.dh), axis=-1)
108
+ y = (att @ v).transpose(1, 0, 2).reshape(T, -1)
109
+ x = x + self._lin(y, self.enc_attn_out)
110
+ h = layer_norm(x, *self.enc_n2)
111
+ x = x + self._lin(np.maximum(self._lin(h, self.enc_ff1), 0.0), self.enc_ff2)
112
+ chunks = x.reshape(self.prefix_len, T // self.prefix_len, self.d).mean(axis=1)
113
+ return layer_norm(tanh_gelu(self._lin(chunks, self.enc_proj)), *self.enc_ln)
114
+
115
+ @staticmethod
116
+ def _lin(x, wb):
117
+ w, b = wb
118
+ return x @ w.T + b
119
+
120
+ # ----------------------------------------------------------- decoder --
121
+ def _block_step(self, i, x, Kc, Vc):
122
+ ln1_w, ln1_b = self.blocks[i]["ln1"]
123
+ h = layer_norm(x, ln1_w, ln1_b)
124
+ qkv = self._lin(h, self.blocks[i]["qkv"])
125
+ q, k, v = np.split(qkv, 3, axis=-1)
126
+ q = q.reshape(self.H, self.dh)
127
+ Kc[i].append(k.reshape(self.H, self.dh))
128
+ Vc[i].append(v.reshape(self.H, self.dh))
129
+ K = np.stack(Kc[i], axis=1) # (H, t, dh)
130
+ V = np.stack(Vc[i], axis=1)
131
+ att = softmax(np.einsum("hd,htd->ht", q, K) / np.sqrt(self.dh), axis=-1)
132
+ y = np.einsum("ht,htd->hd", att, V).reshape(-1)
133
+ x = x + self._lin(y, self.blocks[i]["proj"])
134
+ h2 = tanh_gelu(self._lin(layer_norm(x, *self.blocks[i]["ln2"]),
135
+ self.blocks[i]["fc1"]))
136
+ return x + self._lin(h2, self.blocks[i]["fc2"])
137
+
138
+ # ---------------------------------------------------------- sampling --
139
+ def _sample(self, logits, rng, temperature, top_k, logit_bias=None):
140
+ z = logits.astype(np.float64)
141
+ if logit_bias is not None:
142
+ z = z + np.asarray(logit_bias).reshape(-1).astype(np.float64)
143
+ z = z / max(temperature, 1e-4)
144
+ if top_k and top_k < z.shape[-1]:
145
+ thr = np.partition(z, -top_k)[-top_k]
146
+ z[z < thr] = -np.inf
147
+ p = softmax(z)
148
+ return int(rng.choice(len(p), p=p))
149
+
150
+ def _bias_at(self, logit_bias, j):
151
+ """Flat (32,) bias or per-position (256, 32) matrix row j -> (32,)."""
152
+ if logit_bias is None:
153
+ return None
154
+ arr = np.asarray(logit_bias)
155
+ if arr.ndim == 1:
156
+ return arr
157
+ return arr[min(j, arr.shape[0] - 1)]
158
+
159
+ # ------------------------------------------------------------ public --
160
+ def generate_grid(self, text, seed=0, temperature=None, top_k=None,
161
+ return_logits=False, ids_override=None, logit_bias=None,
162
+ prefix_tokens=None):
163
+ from pxg_tiny.config import encode_caption
164
+ rng = np.random.default_rng(seed)
165
+ temperature = self.temperature if temperature is None else temperature
166
+ top_k = self.top_k if top_k is None else top_k
167
+ if ids_override is not None:
168
+ ids = np.asarray(ids_override, dtype=np.int64)
169
+ else:
170
+ ids = np.array(encode_caption(text), dtype=np.int64)
171
+
172
+ pref = self.encode_text(ids)
173
+ Kc = [[] for _ in range(self.L)]
174
+ Vc = [[] for _ in range(self.L)]
175
+ x = None
176
+ for pi in range(self.prefix_len):
177
+ x = pref[pi] + self.pos_emb[pi]
178
+ for i in range(self.L):
179
+ x = self._block_step(i, x, Kc, Vc)
180
+
181
+ forced = (list(prefix_tokens) if prefix_tokens is not None else [])
182
+ tokens = []
183
+ teacher_logits = []
184
+ for j in range(self.seq_vis):
185
+ if j < len(forced):
186
+ t = int(forced[j]) # structural prior token
187
+ teacher_logits.append(None)
188
+ else:
189
+ logits = self._lin(layer_norm(x, *self.lnf),
190
+ (self.head_w, self.head_b))
191
+ t = self._sample(logits, rng, temperature, top_k,
192
+ logit_bias=self._bias_at(logit_bias, j))
193
+ teacher_logits.append(logits)
194
+ tokens.append(t)
195
+ if j < self.seq_vis - 1:
196
+ x = self.vis_emb[t] + self.pos_emb[self.prefix_len + j + 1]
197
+ for i in range(self.L):
198
+ x = self._block_step(i, x, Kc, Vc)
199
+ grid = np.array(tokens, dtype=np.uint8).reshape(16, 16)
200
+ if return_logits:
201
+ return grid, np.stack(teacher_logits)
202
+ return grid
203
+
204
+ def teacher_forced_logits(self, caps_ids, vis_tokens):
205
+ """Parity hook: feed the exact training layout and return the 256x32
206
+ logits the model assigns for each visual position (prefix only fed
207
+ once, then ground-truth tokens streamed)."""
208
+ Kc = [[] for _ in range(self.L)]
209
+ Vc = [[] for _ in range(self.L)]
210
+ pref = self.encode_text(np.array(caps_ids, dtype=np.int64))
211
+ outs = []
212
+ x = None
213
+ for pi in range(self.prefix_len):
214
+ x = pref[pi] + self.pos_emb[pi]
215
+ for i in range(self.L):
216
+ x = self._block_step(i, x, Kc, Vc)
217
+ outs.append(self._lin(layer_norm(x, *self.lnf), (self.head_w, self.head_b)))
218
+ for j in range(self.seq_vis - 1):
219
+ x = self.vis_emb[int(vis_tokens[j])] + self.pos_emb[self.prefix_len + j]
220
+ for i in range(self.L):
221
+ x = self._block_step(i, x, Kc, Vc)
222
+ outs.append(self._lin(layer_norm(x, *self.lnf), (self.head_w, self.head_b)))
223
+ return np.stack(outs)
224
+
225
+ def generate_rgba(self, text, seed=0, **kw):
226
+ grid = self.generate_grid(text, seed=seed, **kw)
227
+ rgba = np.zeros((16, 16, 4), dtype=np.uint8)
228
+ for idx in range(1, len(self.palette)):
229
+ m = grid == idx
230
+ rgba[m] = self.palette[idx]
231
+ return grid, rgba