Cyronius commited on
Commit
1540c35
·
verified ·
1 Parent(s): 69f2c02

Release category-conditioned sprite EMA weights and browser ONNX exports

Browse files
README.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ tags:
5
+ - image-generation
6
+ - pixel-art
7
+ - rectified-flow
8
+ - onnx
9
+ - webgpu
10
+ pipeline_tag: unconditional-image-generation
11
+ library_name: onnxruntime
12
+ ---
13
+ # Sprite diffusion
14
+
15
+ A 24.2M-parameter, category-conditioned rectified-flow transformer for **32×32 RGBA pixel-art sprites**. This repository distributes inference-only EMA weights for the original 100k model and its 150k continuation, plus ONNX exports for a browser generator. It is not a text-to-image model: it accepts eight category labels, not natural-language prompts.
16
+
17
+ [Code and training write-up](https://github.com/Cyronius/model-experiments/tree/main/sprite-diffusion) · [Browser generator](https://cyronius.github.io/model-experiments/sprite-diffusion/)
18
+
19
+ ## Files and use
20
+
21
+ - `sprite-100k.safetensors`, `sprite-150k.safetensors`: float32 EMA state dictionaries, no optimizer, training arguments, sampler state or datasets.
22
+ - `sprite-100k.onnx`, `sprite-150k.onnx`: float32 velocity networks, opset 17, fixed batch two for conditional/unconditional classifier-free guidance.
23
+ - Matching JSON files record architecture, artifact hashes and local PyTorch/ONNX parity checks.
24
+ - `dit.py` defines the PyTorch architecture. Use `DiT(384, 10, 2)`, load a safetensors state dictionary, then run Euler integration from Gaussian noise at t=1 to t=0.
25
+
26
+ ONNX inputs: `x` float32 `[2,4,32,32]`, `t` float32 `[2]`, `category` int64 `[2]`, `footprint` int64 `[2]`. The output is `velocity` float32 `[2,4,32,32]`. For a category c, duplicate the latent into both batch rows, use category `[c,8]`, footprint `[3,3]`, and combine velocities as `unconditional + 3 * (conditional - unconditional)`. Take a negative Euler step of `1 / steps`. Scale the final clamped [-1,1] RGBA to [0,255], with alpha threshold 128.
27
+
28
+ Categories, in order: creature, item, weapon, food, effect, terrain, structure, nature. The food category includes drinks and potions.
29
+
30
+ **Sampling:** 32 steps is the owner's preferred default; 16 is balanced and 8 is fast. 1–2 steps produce underdeveloped outputs. 64 is not reliably better. Keep both checkpoints: a qualitative same-seed comparison did not establish a universal winner. The 150k model showed more nature variety, while the original retained more varied characters in the inspected seeds.
31
+
32
+ ## Training and evaluation
33
+
34
+ Hidden width 384, 10 blocks, three 128-dimensional attention heads, patch size 2 and 256 tokens. The original checkpoint ran for 100k updates. The continuation used an expanded 383,660-image/frame dataset, 3e-5 learning rate, category/subtype-balanced sampling and palette augmentation: a 10k-update pilot reset the optimizer, then another 40k resumed that pilot. Creature subtypes affect exposure only; they are not output labels.
35
+
36
+ The comparison used 100k/110k/150k checkpoints at 1,2,4,8,12,16,32,64 steps, 64 sprites per setting, two fixed seed sets, CFG 3, EMA weights and no palette snapping. One conversational vision review inspected 1,536 generated sprites. It was not an independent blinded evaluation. Unseen validation and a matched uniform control remain outstanding; no new formal copying or diversity evaluation is claimed.
37
+
38
+ ## Data provenance, licensing and limitations
39
+
40
+ **No training data is distributed.** The corpus combined game-sprite extractions and art packs from mixed sources. The expanded archive contains 20,542 TSR entries, 352,465 OGA entries, 9,597 pack entries and 1,056 curated CC0 entries. These are stored image/frame counts, not distinct designs or training exposure. The supplement's CC0 records do not establish licenses for the rest of the corpus. Source-permission and attribution records are incomplete, and some historical pack licenses were not verified.
41
+
42
+ **No verified redistribution license for these weights is asserted by this model card.** The source repository's Apache-2.0 license covers its code; do not assume it grants rights in the training artwork, weights, or generated outputs. Public availability is not a representation of third-party permission or commercial-use clearance. Consider the underlying rights and attribution obligations before reuse.
43
+
44
+ Outputs can be incomplete, noisy or resemble familiar game-art conventions. Creature output favors people; food often resembles potions. Terrain is not guaranteed to tile. The model does not produce coherent animation sequences. Select and edit results rather than treating every output as a production-ready asset. No output non-infringement guarantee is provided.
45
+
46
+ The original 100k model was tested on AMD XDNA2 NPU hardware: approximately 5.0 s for a 64-sprite sheet at 8 steps and 9.9 s at 16 steps. These historical timings are not browser benchmarks or measurements of the 150k continuation. Browser inference support and speed vary by device.
dit.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Klein-style single-stream DiT for 32x32 RGBA sprites, shared by train.py and prof_train.py; 2026-10-17.
2
+ import math
3
+ import torch, torch.nn as nn, torch.nn.functional as F
4
+
5
+ HEAD = 128
6
+ NCAT, NFP = 8, 3
7
+ # prof_train.py flips these to measure them; 'off' settings exist only for timing.
8
+ FAST = {'norm': True, 'rope': 'stack', 'qknorm': True, 'joint_qk': False, 'addcmul': False}
9
+
10
+
11
+ class RMSNorm(nn.Module):
12
+ def __init__(s, d):
13
+ super().__init__(); s.w = nn.Parameter(torch.ones(d))
14
+
15
+ def forward(s, x):
16
+ if FAST['norm']:
17
+ return F.rms_norm(x, (x.shape[-1],), s.w, 1e-6)
18
+ return x * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + 1e-6).to(x.dtype) * s.w
19
+
20
+
21
+ def rope_table(side):
22
+ # Half of each head encodes the row, half the column (2D RoPE, as in klein).
23
+ f = 1.0 / (10000 ** (torch.arange(0, HEAD // 4) / (HEAD // 4)))
24
+ ys, xs = torch.meshgrid(torch.arange(side), torch.arange(side), indexing='ij')
25
+ ang = torch.cat([ys.reshape(-1, 1) * f, xs.reshape(-1, 1) * f], 1)
26
+ return torch.cos(ang), torch.sin(ang)
27
+
28
+
29
+ def rope(x, cos, sin):
30
+ if FAST['rope'] == 'off':
31
+ return x
32
+ if FAST['rope'] == 'half':
33
+ # Rotate-half pairing (dim i with i+64): fewer passes over q/k than the interleaved form.
34
+ c, s_ = torch.cat([cos, cos], -1).to(x.dtype), torch.cat([sin, sin], -1).to(x.dtype)
35
+ x1, x2 = x.chunk(2, -1)
36
+ return x * c + torch.cat([-x2, x1], -1) * s_
37
+ if FAST['rope'] == 'stack':
38
+ a, b = x.unflatten(-1, (-1, 2)).unbind(-1)
39
+ cos, sin = cos.to(x.dtype), sin.to(x.dtype)
40
+ return torch.stack([a * cos - b * sin, a * sin + b * cos], -1).flatten(-2)
41
+ a, b = x.float().unflatten(-1, (-1, 2)).unbind(-1)
42
+ return torch.stack([a * cos - b * sin, a * sin + b * cos], -1).flatten(-2).to(x.dtype)
43
+
44
+
45
+ class Block(nn.Module):
46
+ def __init__(s, h):
47
+ super().__init__()
48
+ s.h, s.nh, s.mlp = h, h // HEAD, 3 * h
49
+ s.norm = nn.LayerNorm(h, elementwise_affine=False, eps=1e-6)
50
+ s.mod = nn.Linear(h, 3 * h)
51
+ s.lin1 = nn.Linear(h, 3 * h + 2 * s.mlp)
52
+ s.qn, s.kn = RMSNorm(HEAD), RMSNorm(HEAD)
53
+ s.lin2 = nn.Linear(h + s.mlp, h)
54
+ nn.init.zeros_(s.mod.weight); nn.init.zeros_(s.mod.bias)
55
+
56
+ def forward(s, x, c, cos, sin):
57
+ B, T, _ = x.shape
58
+ sh, sc, g = s.mod(F.silu(c)).unsqueeze(1).chunk(3, -1)
59
+ xm = torch.addcmul(sh, s.norm(x), 1 + sc) if FAST['addcmul'] else s.norm(x) * (1 + sc) + sh
60
+ qkv, a, b = s.lin1(xm).split([3 * s.h, s.mlp, s.mlp], -1)
61
+ qkv = qkv.view(B, T, 3, s.nh, HEAD).permute(2, 0, 3, 1, 4)
62
+ v = qkv[2]
63
+ if FAST['joint_qk']:
64
+ qk = qkv[:2]
65
+ if FAST['qknorm']:
66
+ qk = F.rms_norm(qk, (HEAD,), eps=1e-6) * torch.stack([s.qn.w, s.kn.w]).view(2, 1, 1, 1, HEAD).to(qk.dtype)
67
+ q, k = rope(qk, cos, sin).unbind(0)
68
+ else:
69
+ q, k = qkv[0], qkv[1]
70
+ if FAST['qknorm']:
71
+ q, k = s.qn(q), s.kn(k)
72
+ q, k = rope(q, cos, sin), rope(k, cos, sin)
73
+ o = F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(B, T, s.h)
74
+ out = s.lin2(torch.cat([o, F.silu(a) * b], -1))
75
+ return torch.addcmul(x, g, out) if FAST['addcmul'] else x + g * out
76
+
77
+
78
+ class DiT(nn.Module):
79
+ def __init__(s, h, depth, patch, vocab=0):
80
+ super().__init__()
81
+ s.p, s.side = patch, 32 // patch
82
+ s.inp = nn.Linear(4 * patch * patch, h)
83
+ s.t1, s.t2 = nn.Linear(256, h), nn.Linear(h, h)
84
+ s.cat = nn.Embedding(NCAT + 1, h) # last index = no label
85
+ s.fp = nn.Embedding(NFP + 1, h)
86
+ if vocab: # caption words: learned embeddings, mean-pooled, 0 = padding/none
87
+ s.wemb = nn.Embedding(vocab, h, padding_idx=0)
88
+ s.wproj = nn.Sequential(nn.Linear(h, h), nn.SiLU(), nn.Linear(h, h))
89
+ s.blocks = nn.ModuleList([Block(h) for _ in range(depth)])
90
+ s.fnorm = nn.LayerNorm(h, elementwise_affine=False, eps=1e-6)
91
+ s.fmod = nn.Linear(h, 2 * h)
92
+ s.outp = nn.Linear(h, 4 * patch * patch)
93
+ for m in (s.fmod, s.outp):
94
+ nn.init.zeros_(m.weight); nn.init.zeros_(m.bias)
95
+ cos, sin = rope_table(s.side)
96
+ s.register_buffer('cos', cos, persistent=False); s.register_buffer('sin', sin, persistent=False)
97
+
98
+ def forward(s, x, t, cat, fp, txt=None):
99
+ B, p, n = x.shape[0], s.p, s.side
100
+ x = x.unfold(2, p, p).unfold(3, p, p).permute(0, 2, 3, 1, 4, 5).reshape(B, n * n, 4 * p * p)
101
+ half = torch.exp(-math.log(10000) * torch.arange(128, device=x.device) / 128)
102
+ te = torch.cat([torch.cos(t[:, None] * 1000 * half), torch.sin(t[:, None] * 1000 * half)], -1)
103
+ c = s.t2(F.silu(s.t1(te))) + s.cat(cat) + s.fp(fp)
104
+ if txt is not None:
105
+ m = (txt > 0).to(c.dtype)
106
+ e = (s.wemb(txt) * m[..., None]).sum(1) / m.sum(1, keepdim=True).clamp(min=1)
107
+ c = c + s.wproj(e) * (m.sum(1, keepdim=True) > 0)
108
+ x = s.inp(x)
109
+ for blk in s.blocks:
110
+ x = blk(x, c, s.cos, s.sin)
111
+ sh, sc = s.fmod(F.silu(c)).unsqueeze(1).chunk(2, -1)
112
+ x = s.outp(s.fnorm(x) * (1 + sc) + sh)
113
+ return x.view(B, n, n, 4, p, p).permute(0, 3, 1, 4, 2, 5).reshape(B, 4, 32, 32)
sprite-100k.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sprite-100k",
3
+ "architecture": {
4
+ "hidden": 384,
5
+ "depth": 10,
6
+ "patch": 2,
7
+ "channels": 4,
8
+ "canvas": 32
9
+ },
10
+ "weights": "EMA",
11
+ "cfg_batch": 2,
12
+ "dtype": "float32",
13
+ "opset": 17,
14
+ "velocity_relative_errors": [
15
+ 5.293890126267797e-07,
16
+ 2.8245901830814546e-06,
17
+ 1.3860703802492935e-06
18
+ ],
19
+ "euler_16_relative_error": 7.527040725108236e-05,
20
+ "files": {
21
+ "sprite-100k.onnx": {
22
+ "bytes": 102321347,
23
+ "sha256": "3a4cc151d353ebdcee2a01e9b7cd999eb2eb760f545585c6b065ccd8ee138c06"
24
+ },
25
+ "sprite-100k.safetensors": {
26
+ "bytes": 96829272,
27
+ "sha256": "c2198937ababc6f50701206079ae4fdefa8a5f6fc358bc2333dadb3190198388"
28
+ }
29
+ }
30
+ }
sprite-100k.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a4cc151d353ebdcee2a01e9b7cd999eb2eb760f545585c6b065ccd8ee138c06
3
+ size 102321347
sprite-100k.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2198937ababc6f50701206079ae4fdefa8a5f6fc358bc2333dadb3190198388
3
+ size 96829272
sprite-150k.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sprite-150k",
3
+ "architecture": {
4
+ "hidden": 384,
5
+ "depth": 10,
6
+ "patch": 2,
7
+ "channels": 4,
8
+ "canvas": 32
9
+ },
10
+ "weights": "EMA",
11
+ "cfg_batch": 2,
12
+ "dtype": "float32",
13
+ "opset": 17,
14
+ "velocity_relative_errors": [
15
+ 9.889696457321406e-07,
16
+ 2.4362118438148173e-06,
17
+ 1.3656183455168502e-06
18
+ ],
19
+ "euler_16_relative_error": 1.3164431948098354e-05,
20
+ "files": {
21
+ "sprite-150k.onnx": {
22
+ "bytes": 102321347,
23
+ "sha256": "f6d6f3949dddd34de027a62f76e082cc7f12d63a6545feedcc0f4ba13263d3d3"
24
+ },
25
+ "sprite-150k.safetensors": {
26
+ "bytes": 96829272,
27
+ "sha256": "bbe7cedcc7716b98d5dcef6db8088115b6698c31cebce6e80917c888211bc820"
28
+ }
29
+ }
30
+ }
sprite-150k.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6d6f3949dddd34de027a62f76e082cc7f12d63a6545feedcc0f4ba13263d3d3
3
+ size 102321347
sprite-150k.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bbe7cedcc7716b98d5dcef6db8088115b6698c31cebce6e80917c888211bc820
3
+ size 96829272