TobiasLogic commited on
Commit
49ce4bd
·
verified ·
1 Parent(s): 49e84f1

VoxelModel v1: tiny text-to-3D voxel diffusion

Browse files
Files changed (13) hide show
  1. .gitattributes +1 -0
  2. README.md +149 -0
  3. build_data.py +61 -0
  4. caption_filter.py +64 -0
  5. config.json +14 -0
  6. examples.png +0 -0
  7. model.png +3 -0
  8. model.safetensors +3 -0
  9. model_png.json +1 -0
  10. png_codec.py +84 -0
  11. sample.py +73 -0
  12. train.py +186 -0
  13. voxel_dit.py +134 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ pipeline_tag: text-to-3d
4
+ language:
5
+ - en
6
+ tags:
7
+ - 3d
8
+ - text-to-3d
9
+ - voxel
10
+ - diffusion
11
+ - rectified-flow
12
+ - custom-code
13
+ - tiny
14
+ ---
15
+
16
+ # VoxelModel v1 🧊
17
+
18
+ <img src="examples.png" alt="Twelve 32x32x32 grids generated from text prompts" style="width: 640px; max-width: 100%;">
19
+
20
+ A tiny text-to-3D diffusion model. You give it a prompt, it gives you a
21
+ 32x32x32 occupancy grid. It is 40,050,892 trained parameters and it cost
22
+ $3.01 to train.
23
+
24
+ This is the PixelModel line stepping out of 2D. v1 through v4 were tiny latent
25
+ diffusion for images; VoxelModel keeps the same argument (low resolution as a
26
+ deliberate aesthetic, not a compromise) and applies it to shape. `model.png` is
27
+ still here and still holds every weight, because that gimmick ports unchanged to
28
+ 3D: it only ever cared about floats.
29
+
30
+ ## The one honest result
31
+
32
+ It works for bulky objects and it fails for thin ones.
33
+
34
+ Chairs, benches, tables, mushrooms, trees, cars and boats come back legible. A
35
+ sword comes back as 0.2% occupancy, which is a handful of stray voxels, and a key
36
+ comes back as a featureless bar.
37
+
38
+ That is not a training failure. A sword does not survive voxelization at 32^3 in
39
+ the **training data** either: the real Objaverse sword we voxelized for reference
40
+ sits at 2.9% occupancy and reads as a cross at best. The model reproduced what it
41
+ was shown. If you want swords, the answer is resolution, not more steps.
42
+
43
+ So: 32^3 is enough, for a specific and checkable definition of enough. That was
44
+ the question v1 existed to answer.
45
+
46
+ ## No VAE
47
+
48
+ VoxelModel operates directly on voxels, so there is no autoencoder in the stack.
49
+ Total footprint is 40.05M trained plus 37.8M frozen CLIP ViT-B/32, which is
50
+ 77.9M. PixelModel v4 was 161M for comparable trained size, because it carries an
51
+ 83.7M frozen `sd-vae-ft-mse`. This one is tiny without an asterisk.
52
+
53
+ ## Architecture
54
+
55
+ `VoxelDiT` is a DiT with 3D patchify. A 32^3 grid at patch 4 gives 512 tokens,
56
+ dim 384, depth 12, heads 6. Conditioning is cross-attention on frozen CLIP
57
+ ViT-B/32 text features (40 tokens), plus adaLN-zero from the pooled embedding and
58
+ the timestep. Positional encoding is a 3D sincos over the 8x8x8 patch grid.
59
+
60
+ Objective is rectified flow with `target = x1 - x0` and logit-normal timestep
61
+ sampling, `t = sigmoid(randn)`.
62
+
63
+ ## Training
64
+
65
+ | | |
66
+ |---|---|
67
+ | meshes | 28,415 train, 1,024 held out |
68
+ | steps | 90,000 at batch 256 |
69
+ | hardware | one A100 80GB, 2.57 steps/s, 41.4 GiB |
70
+ | wall clock | 9.7 hours, $3.01 |
71
+ | optimizer | AdamW 2e-4, betas (0.9, 0.99), grad clip 1.0 |
72
+ | schedule | cosine to 1e-6, 500 step warmup |
73
+ | EMA | 0.9999 |
74
+ | CFG dropout | 10% |
75
+
76
+ Held-out loss bottomed at **0.018737 at step 64,000** and drifted up to 0.019314
77
+ by step 90,000. That is 3.08% of overfitting, and it means the run was about 2.8
78
+ hours longer than it needed to be. 90,000 steps over 28,415 meshes is 811 epochs
79
+ with no augmentation, which is the obvious thing to fix next. Yaw rotations plus
80
+ a mirror would give 8x for free on a cubic grid.
81
+
82
+ The shipped weights are the step 90,000 EMA, not the step 64,000 best, because
83
+ checkpoints were written to a fixed path and overwrote each other. Noted, and
84
+ worth 3.08% to you if you retrain.
85
+
86
+ ## Data
87
+
88
+ Meshes are `dylanebert/objaverse-lowpoly-obj`, captions are `tiange/Cap3D`,
89
+ joined on Objaverse UID for 58,374 captioned meshes. Voxelization centers on the
90
+ vertex mean, scales by max absolute coordinate, and rasterizes surfaces at pitch
91
+ 2/31 with no fill, since filling solidifies closed volumes and pushes 4% of the
92
+ set past 90% occupancy.
93
+
94
+ `caption_filter.py` then throws away half of it. Multi-object meshes are the main
95
+ defect in this corpus: normalization scales the whole scene bounding box into the
96
+ grid, so a mesh holding several objects shrinks each one into a cluster of blobs.
97
+ The caption gives this away before you load anything, so the filter rejects scene
98
+ and variant-set wording, a top level `and` joining two noun phrases, and captions
99
+ over 12 words. Colour pairs survive on purpose, so "a purple and green mushroom"
100
+ is kept and "a white cube and a white robot" is not.
101
+
102
+ Measured over all 58,374 captions, 29,439 survive, which is 50.4%.
103
+
104
+ Worth knowing if you build on this: **occupancy does not detect the defect.**
105
+ Survivors and rejects have almost the same occupancy distribution, medians 4.3%
106
+ against 5.0% over a 900 mesh sample. No threshold would have caught it.
107
+
108
+ ## Usage
109
+
110
+ ```bash
111
+ python sample.py "a wooden chair" "a purple mushroom" --cfg 5.0 --steps 50
112
+ ```
113
+
114
+ Writes a JSON of base64 `np.packbits` grids. To load the weights from the PNG
115
+ instead of the safetensors:
116
+
117
+ ```python
118
+ from png_codec import load_model_png
119
+ model = load_model_png("model.png", "model_png.json", device="cuda")
120
+ ```
121
+
122
+ Round-trip error against the safetensors is 9.7e-04 max absolute, which is fp16
123
+ quantization and nothing else.
124
+
125
+ ## What v1 does not do
126
+
127
+ **Colour.** These meshes carry no vertex colours and no `.mtl` files, so the
128
+ model generates grey geometry only. Colour needs a GLB-with-materials source and
129
+ its own voxelization path.
130
+
131
+ **A real eval metric.** FID does not transfer to 3D. Held-out rectified flow loss
132
+ is what the table above reports, and it is a training diagnostic, not a quality
133
+ measure. Rendered-FID with CLIP score on 2D renders, or IoU against a held-out
134
+ split, are the candidates. This is unresolved and we would rather say so than
135
+ publish a number we do not believe.
136
+
137
+ ## Files
138
+
139
+ | file | what |
140
+ |---|---|
141
+ | `model.safetensors` | EMA weights, fp32 |
142
+ | `model.png` | the same weights as a 6329x6329 PNG |
143
+ | `model_png.json` | parameter manifest for the PNG |
144
+ | `voxel_dit.py` | the model |
145
+ | `train.py` | training loop |
146
+ | `sample.py` | inference |
147
+ | `caption_filter.py` | the single-object filter |
148
+ | `build_data.py` | mesh download and voxelization |
149
+ | `png_codec.py` | PNG encode, decode and verify |
build_data.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, os, sys, time, numpy as np, trimesh
2
+ from multiprocessing import Pool
3
+ sys.path.insert(0, '/root')
4
+ from caption_filter import is_single_object
5
+
6
+ root = '/root/lowpoly'
7
+ OUT = '/root/data'
8
+ os.makedirs(OUT, exist_ok=True)
9
+
10
+ paths = {}
11
+ for dirpath, _, names in os.walk(root):
12
+ for n in names:
13
+ if n.endswith('.obj'):
14
+ paths[n[:-4]] = os.path.join(dirpath, n)
15
+ print('obj files on disk:', len(paths), flush=True)
16
+
17
+ caps = json.load(open('/root/captions.json'))
18
+ uids = sorted(u for u in caps if u in paths)
19
+ print('captioned meshes:', len(uids), flush=True)
20
+
21
+ def vox(u):
22
+ try:
23
+ m = trimesh.load(paths[u], force='mesh')
24
+ v = m.vertices - m.vertices.mean(0)
25
+ s = np.abs(v).max()
26
+ m.vertices = v / (s if s > 0 else 1)
27
+ g = m.voxelized(pitch=2.0/31).matrix.astype(np.uint8)
28
+ pad = np.zeros((32,32,32), np.uint8)
29
+ sh = [min(32,d) for d in g.shape]
30
+ pad[:sh[0],:sh[1],:sh[2]] = g[:sh[0],:sh[1],:sh[2]]
31
+ if pad.sum() == 0:
32
+ return u, None
33
+ return u, np.packbits(pad.reshape(-1))
34
+ except Exception:
35
+ return u, None
36
+
37
+ t0 = time.time()
38
+ ok_uids, rows, failed = [], [], 0
39
+ with Pool(32) as p:
40
+ for i, (u, packed) in enumerate(p.imap(vox, uids, chunksize=64)):
41
+ if packed is None:
42
+ failed += 1
43
+ else:
44
+ ok_uids.append(u); rows.append(packed)
45
+ if (i+1) % 5000 == 0:
46
+ print(f' voxelized {i+1}/{len(uids)} failed {failed} {time.time()-t0:.0f}s', flush=True)
47
+
48
+ vox_arr = np.stack(rows)
49
+ np.save(f'{OUT}/voxels_packed.npy', vox_arr)
50
+ captions = [caps[u] for u in ok_uids]
51
+ keep = [bool(is_single_object(c)) for c in captions]
52
+ json.dump({'uids': ok_uids, 'captions': captions, 'keep': keep},
53
+ open(f'{OUT}/meta.json', 'w'))
54
+
55
+ occ = np.unpackbits(vox_arr, axis=1).mean(1) * 100
56
+ print(f'\ndone in {time.time()-t0:.0f}s')
57
+ print(f'meshes voxelized {len(ok_uids)}, failed/empty {failed}')
58
+ print(f'array {vox_arr.shape} {vox_arr.nbytes/2**20:.0f} MiB packed')
59
+ print(f'filter survivors {sum(keep)} = {sum(keep)/len(keep)*100:.1f}%')
60
+ print(f'surface occupancy mean {occ.mean():.2f}% median {np.median(occ):.2f}%')
61
+ print('BUILDDONE', flush=True)
caption_filter.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ MAX_WORDS = 12
6
+
7
+ SCENE_PATTERNS = [
8
+ ("variation", r"\bvari(?:ation|ations|ant|ants|ous)\b"),
9
+ ("set of", r"\bsets?\s+of\b"),
10
+ ("collection", r"\bcollections?\b"),
11
+ ("assorted", r"\bassorted\b"),
12
+ ("multiple", r"\bmultiple\b"),
13
+ ("pair of", r"\bpairs?\s+of\b"),
14
+ ("group of", r"\bgroups?\s+of\b"),
15
+ ("scene", r"\bscenes?\b"),
16
+ ("elements", r"\belements?\b"),
17
+ ("featuring", r"\bfeaturing\b"),
18
+ ("different", r"\bdifferent\b"),
19
+ ]
20
+
21
+ MODIFIERS = {
22
+ "black", "white", "red", "blue", "green", "yellow", "orange", "purple",
23
+ "pink", "brown", "gray", "grey", "gold", "golden", "silver", "bronze",
24
+ "copper", "beige", "tan", "teal", "cyan", "magenta", "maroon", "navy",
25
+ "ivory", "cream", "turquoise", "lavender", "olive", "crimson", "scarlet",
26
+ "dark", "light", "pale", "bright", "deep", "pastel", "neon", "striped",
27
+ "wooden", "wood", "metal", "metallic", "plastic", "stone", "glass",
28
+ "steel", "iron", "brick", "concrete", "leather", "fabric", "marble",
29
+ }
30
+
31
+ _WORD = re.compile(r"[a-z][a-z'-]*")
32
+ _AND = re.compile(r"\band\b")
33
+
34
+ def _joins_noun_phrases(text):
35
+ parts = _AND.split(text)
36
+ if len(parts) < 2:
37
+ return False
38
+ for left, right in zip(parts[:-1], parts[1:]):
39
+ if "," in left:
40
+ return True
41
+ lw = _WORD.findall(left)
42
+ rw = _WORD.findall(right)
43
+ if not lw or not rw:
44
+ return True
45
+ if lw[-1] in MODIFIERS and rw[0] in MODIFIERS:
46
+ continue
47
+ return True
48
+ return False
49
+
50
+ def reject_reason(caption):
51
+ if not caption or not caption.strip():
52
+ return "empty"
53
+ text = caption.lower()
54
+ if len(caption.split()) > MAX_WORDS:
55
+ return "too long"
56
+ for name, pattern in SCENE_PATTERNS:
57
+ if re.search(pattern, text):
58
+ return name
59
+ if _joins_noun_phrases(text):
60
+ return "and"
61
+ return None
62
+
63
+ def is_single_object(caption):
64
+ return reject_reason(caption) is None
config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dit": {
3
+ "vox_size": 32,
4
+ "patch": 4,
5
+ "dim": 384,
6
+ "depth": 12,
7
+ "heads": 6,
8
+ "text_dim": 512
9
+ },
10
+ "steps": 90000,
11
+ "batch_size": 256,
12
+ "meshes": 28415,
13
+ "filtered": true
14
+ }
examples.png ADDED
model.png ADDED

Git LFS Details

  • SHA256: 901a178d575b71dea6bd74ec3e9a9114302674739764def200cb7ce98ca74092
  • Pointer size: 133 Bytes
  • Size of remote file: 89.7 MB
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cce2211e7e7500dd661df15e64e185770046879073394e5a4a31a0185331f0a
3
+ size 161012360
model_png.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"cfg": {"vox_size": 32, "patch": 4, "dim": 384, "depth": 12, "heads": 6, "text_dim": 512}, "params": [{"name": "x_embed.weight", "shape": [384, 64], "numel": 24576}, {"name": "x_embed.bias", "shape": [384], "numel": 384}, {"name": "t_mlp.0.weight", "shape": [384, 384], "numel": 147456}, {"name": "t_mlp.0.bias", "shape": [384], "numel": 384}, {"name": "t_mlp.2.weight", "shape": [384, 384], "numel": 147456}, {"name": "t_mlp.2.bias", "shape": [384], "numel": 384}, {"name": "text_proj.weight", "shape": [384, 512], "numel": 196608}, {"name": "text_proj.bias", "shape": [384], "numel": 384}, {"name": "text_pool.weight", "shape": [384, 512], "numel": 196608}, {"name": "text_pool.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.0.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.0.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.0.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.0.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.0.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.0.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.0.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.0.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.0.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.0.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.0.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.0.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.0.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.0.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.1.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.1.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.1.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.1.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.1.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.1.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.1.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.1.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.1.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.1.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.1.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.1.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.1.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.1.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.1.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.1.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.1.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.1.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.1.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.2.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.2.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.2.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.2.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.2.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.2.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.2.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.2.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.2.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.2.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.2.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.2.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.2.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.2.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.2.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.2.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.2.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.2.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.2.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.3.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.3.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.3.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.3.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.3.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.3.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.3.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.3.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.3.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.3.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.3.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.3.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.3.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.3.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.3.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.3.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.3.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.3.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.3.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.4.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.4.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.4.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.4.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.4.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.4.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.4.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.4.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.4.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.4.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.4.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.4.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.4.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.4.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.4.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.4.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.4.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.4.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.4.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.5.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.5.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.5.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.5.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.5.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.5.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.5.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.5.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.5.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.5.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.5.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.5.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.5.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.5.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.5.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.5.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.5.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.5.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.5.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.6.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.6.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.6.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.6.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.6.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.6.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.6.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.6.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.6.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.6.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.6.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.6.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.6.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.6.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.6.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.6.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.6.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.6.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.6.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.7.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.7.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.7.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.7.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.7.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.7.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.7.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.7.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.7.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.7.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.7.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.7.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.7.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.7.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.7.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.7.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.7.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.7.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.7.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.8.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.8.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.8.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.8.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.8.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.8.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.8.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.8.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.8.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.8.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.8.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.8.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.8.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.8.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.8.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.8.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.8.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.8.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.8.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.9.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.9.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.9.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.9.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.9.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.9.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.9.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.9.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.9.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.9.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.9.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.9.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.9.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.9.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.9.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.9.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.9.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.9.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.9.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.10.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.10.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.10.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.10.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.10.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.10.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.10.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.10.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.10.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.10.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.10.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.10.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.10.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.10.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.10.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.10.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.10.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.10.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.10.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "blocks.11.cross_gate", "shape": [1], "numel": 1}, {"name": "blocks.11.attn.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.11.attn.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.11.attn.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.11.attn.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.11.attn.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.11.attn.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.11.cross.q.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.11.cross.q.bias", "shape": [384], "numel": 384}, {"name": "blocks.11.cross.kv.weight", "shape": [768, 384], "numel": 294912}, {"name": "blocks.11.cross.kv.bias", "shape": [768], "numel": 768}, {"name": "blocks.11.cross.proj.weight", "shape": [384, 384], "numel": 147456}, {"name": "blocks.11.cross.proj.bias", "shape": [384], "numel": 384}, {"name": "blocks.11.mlp.0.weight", "shape": [1536, 384], "numel": 589824}, {"name": "blocks.11.mlp.0.bias", "shape": [1536], "numel": 1536}, {"name": "blocks.11.mlp.2.weight", "shape": [384, 1536], "numel": 589824}, {"name": "blocks.11.mlp.2.bias", "shape": [384], "numel": 384}, {"name": "blocks.11.ada.1.weight", "shape": [2304, 384], "numel": 884736}, {"name": "blocks.11.ada.1.bias", "shape": [2304], "numel": 2304}, {"name": "ada_out.1.weight", "shape": [768, 384], "numel": 294912}, {"name": "ada_out.1.bias", "shape": [768], "numel": 768}, {"name": "head.weight", "shape": [64, 384], "numel": 24576}, {"name": "head.bias", "shape": [64], "numel": 64}], "total_parameters": 40050892, "side": 6329, "dtype": "float16", "channels": "R=hi,G=lo,B=unused"}
png_codec.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import math
6
+ import os
7
+
8
+ import numpy as np
9
+ import torch
10
+ from PIL import Image
11
+ from safetensors.torch import load_file
12
+
13
+ from voxel_dit import VoxelDiT
14
+
15
+ def encode(weights, png_path, cfg_path, config="config.json"):
16
+ c = json.load(open(config))["dit"]
17
+ model = VoxelDiT(vox_size=c["vox_size"], patch=c["patch"], dim=c["dim"],
18
+ depth=c["depth"], heads=c["heads"], text_dim=c["text_dim"])
19
+ model.load_state_dict(load_file(weights))
20
+ parts, manifest = [], []
21
+ for name, p in model.named_parameters():
22
+ a = p.detach().to(torch.float16).contiguous().view(-1).numpy()
23
+ parts.append(a)
24
+ manifest.append({"name": name, "shape": list(p.shape), "numel": int(a.size)})
25
+ flat = np.concatenate(parts)
26
+ N = flat.size
27
+ side = math.ceil(math.sqrt(N))
28
+ u16 = flat.view(np.uint16)
29
+ img = np.zeros((side * side, 3), dtype=np.uint8)
30
+ img[:N, 0] = (u16 >> 8).astype(np.uint8)
31
+ img[:N, 1] = (u16 & 0xFF).astype(np.uint8)
32
+ Image.fromarray(img.reshape(side, side, 3), "RGB").save(png_path)
33
+ total = sum(m["numel"] for m in manifest)
34
+ json.dump({"cfg": c, "params": manifest, "total_parameters": total,
35
+ "side": side, "dtype": "float16", "channels": "R=hi,G=lo,B=unused"},
36
+ open(cfg_path, "w"))
37
+ mb = os.path.getsize(png_path) / 1e6
38
+ print(f"[png] encoded {total:,} params -> {side}x{side} PNG ({mb:.1f} MB)")
39
+ return total, side
40
+
41
+ def load_model_png(png_path, cfg_path, device="cpu"):
42
+ meta = json.load(open(cfg_path))
43
+ c = meta["cfg"]
44
+ model = VoxelDiT(vox_size=c["vox_size"], patch=c["patch"], dim=c["dim"],
45
+ depth=c["depth"], heads=c["heads"], text_dim=c["text_dim"])
46
+ arr = np.asarray(Image.open(png_path).convert("RGB")).reshape(-1, 3)
47
+ total = meta["total_parameters"]
48
+ hi = arr[:total, 0].astype(np.uint16)
49
+ lo = arr[:total, 1].astype(np.uint16)
50
+ flat = ((hi << 8) | lo).astype(np.uint16).view(np.float16)
51
+ sd = dict(model.named_parameters())
52
+ off = 0
53
+ with torch.no_grad():
54
+ for m in meta["params"]:
55
+ n = m["numel"]
56
+ chunk = flat[off:off + n].astype(np.float16)
57
+ sd[m["name"]].copy_(torch.from_numpy(chunk.copy()).view(*m["shape"]).to(torch.float32))
58
+ off += n
59
+ return model.to(device).eval()
60
+
61
+ def verify(weights, png_path, cfg_path, config="config.json"):
62
+ model = load_model_png(png_path, cfg_path)
63
+ ref = load_file(weights)
64
+ worst, name = 0.0, ""
65
+ for k, p in model.named_parameters():
66
+ d = (p.detach() - ref[k].float()).abs().max().item()
67
+ if d > worst:
68
+ worst, name = d, k
69
+ rel = worst / max(1e-12, ref[name].float().abs().max().item())
70
+ print(f"[png] round-trip max abs err {worst:.3e} on {name} (relative {rel:.2e})")
71
+ return worst
72
+
73
+ def main():
74
+ ap = argparse.ArgumentParser()
75
+ ap.add_argument("--weights", default="model.safetensors")
76
+ ap.add_argument("--png", default="model.png")
77
+ ap.add_argument("--manifest", default="model_png.json")
78
+ ap.add_argument("--config", default="config.json")
79
+ args = ap.parse_args()
80
+ encode(args.weights, args.png, args.manifest, args.config)
81
+ verify(args.weights, args.png, args.manifest, args.config)
82
+
83
+ if __name__ == "__main__":
84
+ main()
sample.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ import json
6
+ import os
7
+
8
+ import numpy as np
9
+ import torch
10
+ from safetensors.torch import load_file
11
+ from transformers import CLIPTextModel, CLIPTokenizer
12
+
13
+ from voxel_dit import VoxelDiT
14
+
15
+ MAX_TOKENS = 40
16
+
17
+ @torch.no_grad()
18
+ def sample(model, seq, pool, null_seq, null_pool, steps, cfg, dev):
19
+ B = seq.shape[0]
20
+ x = torch.randn(B, 1, 32, 32, 32, device=dev)
21
+ ns = null_seq.expand(B, -1, -1)
22
+ npool = null_pool.expand(B, -1)
23
+ dt = 1.0 / steps
24
+ for i in range(steps):
25
+ t = torch.full((B,), i * dt, device=dev)
26
+ with torch.autocast("cuda", dtype=torch.bfloat16):
27
+ vc = model(x, t, seq, pool)
28
+ vu = model(x, t, ns, npool)
29
+ x = x + (vu + cfg * (vc - vu)).float() * dt
30
+ return x
31
+
32
+ @torch.no_grad()
33
+ def main():
34
+ ap = argparse.ArgumentParser()
35
+ ap.add_argument("prompts", nargs="+")
36
+ ap.add_argument("--out", default="samples.json")
37
+ ap.add_argument("--cfg", type=float, default=5.0)
38
+ ap.add_argument("--steps", type=int, default=50)
39
+ ap.add_argument("--threshold", type=float, default=0.0)
40
+ ap.add_argument("--device", default="cuda")
41
+ ap.add_argument("--weights", default="/root/runs/voxel_v1/model.safetensors")
42
+ ap.add_argument("--clip", default="openai/clip-vit-base-patch32")
43
+ args = ap.parse_args()
44
+ dev = args.device
45
+
46
+ model = VoxelDiT().to(dev).eval()
47
+ model.load_state_dict(load_file(args.weights))
48
+
49
+ tok = CLIPTokenizer.from_pretrained(args.clip)
50
+ txt = CLIPTextModel.from_pretrained(args.clip).to(dev).eval()
51
+
52
+ def enc(strings):
53
+ t = tok(strings, padding="max_length", max_length=MAX_TOKENS,
54
+ truncation=True, return_tensors="pt").to(dev)
55
+ o = txt(**t)
56
+ return o.last_hidden_state.float(), o.pooler_output.float()
57
+
58
+ seq, pool = enc(args.prompts)
59
+ null_seq, null_pool = enc([""])
60
+ x = sample(model, seq, pool, null_seq, null_pool, args.steps, args.cfg, dev)
61
+
62
+ grids = (x[:, 0] > args.threshold).to(torch.uint8).cpu().numpy()
63
+ out = []
64
+ for p, g in zip(args.prompts, grids):
65
+ occ = float(g.mean()) * 100
66
+ out.append({"prompt": p, "occS": round(occ, 1),
67
+ "s": base64.b64encode(np.packbits(g.reshape(-1)).tobytes()).decode()})
68
+ print(f" {occ:5.1f}% {p}")
69
+ json.dump(out, open(args.out, "w"))
70
+ print(f"[sample] {len(out)} grids -> {args.out} (cfg {args.cfg}, {args.steps} steps)")
71
+
72
+ if __name__ == "__main__":
73
+ main()
train.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import copy
5
+ import json
6
+ import math
7
+ import os
8
+ import time
9
+
10
+ import numpy as np
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from safetensors.torch import save_file
14
+
15
+ from voxel_dit import VoxelDiT
16
+
17
+ def cosine_lr(step, total, base, floor, warmup):
18
+ if step < warmup:
19
+ return base * (step + 1) / max(1, warmup)
20
+ t = (step - warmup) / max(1, total - warmup)
21
+ return floor + 0.5 * (base - floor) * (1 + math.cos(math.pi * t))
22
+
23
+ def load_split(data, filtered, n_val, seed=0):
24
+ meta = json.load(open(f"{data}/meta.json"))
25
+ keep = np.array(meta["keep"], bool)
26
+ idx = np.nonzero(keep)[0] if filtered else np.arange(len(keep))
27
+ perm = np.random.RandomState(seed).permutation(len(idx))
28
+ idx = idx[perm]
29
+ val, tr = idx[:n_val], idx[n_val:]
30
+ packed = np.load(f"{data}/voxels_packed.npy")
31
+ seq = np.load(f"{data}/text_seq.npy")
32
+ pool = np.load(f"{data}/text_pool.npy")
33
+ pack = lambda s: (np.unpackbits(packed[s], axis=1), seq[s], pool[s])
34
+ return pack(tr), pack(val), len(keep)
35
+
36
+ def main():
37
+ ap = argparse.ArgumentParser()
38
+ ap.add_argument("--data", default="/root/data")
39
+ ap.add_argument("--out", default="/root/runs/voxel_v1")
40
+ ap.add_argument("--steps", type=int, default=90000)
41
+ ap.add_argument("--batch-size", type=int, default=256)
42
+ ap.add_argument("--lr", type=float, default=2e-4)
43
+ ap.add_argument("--min-lr", type=float, default=1e-6)
44
+ ap.add_argument("--warmup", type=int, default=500)
45
+ ap.add_argument("--cfg-dropout", type=float, default=0.1)
46
+ ap.add_argument("--ema", type=float, default=0.9999)
47
+ ap.add_argument("--filtered", type=int, default=1)
48
+ ap.add_argument("--val-size", type=int, default=1024)
49
+ ap.add_argument("--val-every", type=int, default=2000)
50
+ ap.add_argument("--log-every", type=int, default=200)
51
+ ap.add_argument("--save-every", type=int, default=10000)
52
+ ap.add_argument("--resume", default="")
53
+ ap.add_argument("--seed", type=int, default=0)
54
+ args = ap.parse_args()
55
+
56
+ dev = "cuda"
57
+ os.makedirs(args.out, exist_ok=True)
58
+ torch.manual_seed(args.seed)
59
+ torch.backends.cuda.matmul.allow_tf32 = True
60
+ torch.backends.cudnn.allow_tf32 = True
61
+
62
+ (vox, seq, pool), (vvox, vseq, vpool), total_meshes = load_split(
63
+ args.data, args.filtered, args.val_size)
64
+ M = vox.shape[0]
65
+ vox_t = torch.from_numpy(vox).to(dev)
66
+ seq_t = torch.from_numpy(seq).to(dev)
67
+ pool_t = torch.from_numpy(pool).to(dev)
68
+ null_seq = torch.from_numpy(np.load(f"{args.data}/null_seq.npy")).to(dev).float()
69
+ null_pool = torch.from_numpy(np.load(f"{args.data}/null_pool.npy")).to(dev).float()
70
+
71
+ vg = torch.Generator(device=dev).manual_seed(1234)
72
+ vx1 = torch.from_numpy(vvox).to(dev).view(-1, 1, 32, 32, 32).float() * 2 - 1
73
+ vs = torch.from_numpy(vseq).to(dev).float()
74
+ vp = torch.from_numpy(vpool).to(dev).float()
75
+ vx0 = torch.randn(vx1.shape, device=dev, generator=vg)
76
+ vt = torch.sigmoid(torch.randn(vx1.shape[0], device=dev, generator=vg))
77
+
78
+ @torch.no_grad()
79
+ def val_loss():
80
+ ema.eval()
81
+ tot, n = 0.0, 0
82
+ for j in range(0, vx1.shape[0], args.batch_size):
83
+ sl = slice(j, j + args.batch_size)
84
+ m = vx1[sl].shape[0]
85
+ tb = vt[sl].view(-1, 1, 1, 1, 1)
86
+ xt = (1 - tb) * vx0[sl] + tb * vx1[sl]
87
+ with torch.autocast("cuda", dtype=torch.bfloat16):
88
+ v = ema(xt, vt[sl], vs[sl], vp[sl])
89
+ tot += F.mse_loss(v.float(), vx1[sl] - vx0[sl]).item() * m
90
+ n += m
91
+ return tot / n
92
+
93
+ model = VoxelDiT().to(dev)
94
+ ema = copy.deepcopy(model).eval()
95
+ for p in ema.parameters():
96
+ p.requires_grad_(False)
97
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, betas=(0.9, 0.99), weight_decay=0.0)
98
+
99
+ start = 0
100
+ if args.resume and os.path.exists(args.resume):
101
+ ck = torch.load(args.resume, map_location=dev)
102
+ model.load_state_dict(ck["model"])
103
+ ema.load_state_dict(ck["ema"])
104
+ opt.load_state_dict(ck["opt"])
105
+ start = ck["step"] + 1
106
+
107
+ B = args.batch_size
108
+ gen = torch.Generator(device=dev).manual_seed(args.seed)
109
+ epochs = args.steps * B / M
110
+ print(f"[train] {M} train + {vx1.shape[0]} val of {total_meshes} captioned "
111
+ f"({'filtered' if args.filtered else 'unfiltered'}), "
112
+ f"{model.num_params()/1e6:.2f}M params", flush=True)
113
+ print(f"[train] {args.steps} steps x batch {B} = {epochs:.0f} epochs, "
114
+ f"resume from {start}", flush=True)
115
+
116
+ logf = open(f"{args.out}/log.jsonl", "a")
117
+ running, t0 = 0.0, time.time()
118
+ for step in range(start, args.steps):
119
+ i = torch.randint(0, M, (B,), device=dev, generator=gen)
120
+ x1 = vox_t[i].view(B, 1, 32, 32, 32).float() * 2 - 1
121
+ s = seq_t[i].float()
122
+ p = pool_t[i].float()
123
+ drop = torch.rand(B, device=dev, generator=gen) < args.cfg_dropout
124
+ s = torch.where(drop[:, None, None], null_seq, s)
125
+ p = torch.where(drop[:, None], null_pool, p)
126
+
127
+ x0 = torch.randn(x1.shape, device=dev, generator=gen)
128
+ u = torch.randn(B, device=dev, generator=gen)
129
+ t = torch.sigmoid(u)
130
+ tb = t.view(-1, 1, 1, 1, 1)
131
+ xt = (1 - tb) * x0 + tb * x1
132
+ target = x1 - x0
133
+
134
+ lr = cosine_lr(step, args.steps, args.lr, args.min_lr, args.warmup)
135
+ for g in opt.param_groups:
136
+ g["lr"] = lr
137
+
138
+ with torch.autocast("cuda", dtype=torch.bfloat16):
139
+ v = model(xt, t, s, p)
140
+ loss = F.mse_loss(v.float(), target)
141
+ opt.zero_grad(set_to_none=True)
142
+ loss.backward()
143
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
144
+ opt.step()
145
+ with torch.no_grad():
146
+ d = 1 - args.ema
147
+ for pe, pm in zip(ema.parameters(), model.parameters()):
148
+ pe.mul_(args.ema).add_(pm.detach(), alpha=d)
149
+
150
+ running += loss.item()
151
+ if (step + 1) % args.log_every == 0:
152
+ avg = running / args.log_every
153
+ el = time.time() - t0
154
+ sps = args.log_every / el
155
+ eta = (args.steps - step - 1) / sps / 3600
156
+ print(f"[s{step+1:06d}] loss={avg:.5f} lr={lr:.2e} gnorm={gn:.2f} "
157
+ f"{sps:.2f} steps/s eta={eta:.2f}h", flush=True)
158
+ logf.write(json.dumps({"step": step + 1, "loss": avg, "lr": lr,
159
+ "gnorm": float(gn), "steps_per_s": sps}) + "\n")
160
+ logf.flush()
161
+ running, t0 = 0.0, time.time()
162
+
163
+ if (step + 1) % args.val_every == 0 or step + 1 == args.steps:
164
+ vl = val_loss()
165
+ print(f"[s{step+1:06d}] val_loss={vl:.5f} (ema, {args.val_size} held out)", flush=True)
166
+ logf.write(json.dumps({"step": step + 1, "val_loss": vl}) + "\n")
167
+ logf.flush()
168
+ t0 = time.time()
169
+
170
+ if (step + 1) % args.save_every == 0 or step + 1 == args.steps:
171
+ torch.save({"model": model.state_dict(), "ema": ema.state_dict(),
172
+ "opt": opt.state_dict(), "step": step},
173
+ f"{args.out}/ckpt.pt")
174
+ save_file({k: v.contiguous() for k, v in ema.state_dict().items()},
175
+ f"{args.out}/model.safetensors")
176
+ print(f"[s{step+1:06d}] saved", flush=True)
177
+
178
+ json.dump({"dit": {"vox_size": 32, "patch": 4, "dim": 384, "depth": 12,
179
+ "heads": 6, "text_dim": 512},
180
+ "steps": args.steps, "batch_size": args.batch_size,
181
+ "meshes": M, "filtered": bool(args.filtered)},
182
+ open(f"{args.out}/config.json", "w"), indent=2)
183
+ print("TRAINDONE", flush=True)
184
+
185
+ if __name__ == "__main__":
186
+ main()
voxel_dit.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+ def modulate(x, shift, scale):
10
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
11
+
12
+ def timestep_embedding(t, dim, max_period=10000):
13
+ half = dim // 2
14
+ freqs = torch.exp(-math.log(max_period) * torch.arange(half, device=t.device) / half)
15
+ args = t[:, None].float() * freqs[None]
16
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
17
+ if dim % 2:
18
+ emb = torch.cat([emb, torch.zeros_like(emb[:, :1])], dim=-1)
19
+ return emb
20
+
21
+ def sincos_3d(dim, grid_size):
22
+ assert dim % 6 == 0
23
+ g = np.arange(grid_size, dtype=np.float32)
24
+ gz, gy, gx = np.meshgrid(g, g, g, indexing="ij")
25
+ d6 = dim // 6
26
+ omega = 1.0 / (10000 ** (np.arange(d6, dtype=np.float32) / d6))
27
+ def emb1(p):
28
+ out = p.reshape(-1)[:, None] * omega[None]
29
+ return np.concatenate([np.sin(out), np.cos(out)], axis=1)
30
+ pe = np.concatenate([emb1(gz), emb1(gy), emb1(gx)], axis=1)
31
+ return torch.from_numpy(pe).float()
32
+
33
+ class Attention(nn.Module):
34
+ def __init__(self, dim, heads):
35
+ super().__init__()
36
+ self.heads = heads
37
+ self.q = nn.Linear(dim, dim)
38
+ self.kv = nn.Linear(dim, dim * 2)
39
+ self.proj = nn.Linear(dim, dim)
40
+
41
+ def forward(self, x, ctx=None):
42
+ ctx = x if ctx is None else ctx
43
+ B, N, C = x.shape
44
+ M = ctx.shape[1]
45
+ h = self.heads
46
+ q = self.q(x).reshape(B, N, h, C // h).transpose(1, 2)
47
+ kv = self.kv(ctx).reshape(B, M, 2, h, C // h).permute(2, 0, 3, 1, 4)
48
+ k, v = kv[0], kv[1]
49
+ o = F.scaled_dot_product_attention(q, k, v)
50
+ o = o.transpose(1, 2).reshape(B, N, C)
51
+ return self.proj(o)
52
+
53
+ class Block(nn.Module):
54
+ def __init__(self, dim, heads, mlp_ratio=4.0):
55
+ super().__init__()
56
+ self.norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
57
+ self.attn = Attention(dim, heads)
58
+ self.norm_ca = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
59
+ self.cross = Attention(dim, heads)
60
+ self.norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
61
+ hidden = int(dim * mlp_ratio)
62
+ self.mlp = nn.Sequential(nn.Linear(dim, hidden), nn.GELU(approximate="tanh"),
63
+ nn.Linear(hidden, dim))
64
+ self.ada = nn.Sequential(nn.SiLU(), nn.Linear(dim, 6 * dim))
65
+ self.cross_gate = nn.Parameter(torch.zeros(1))
66
+
67
+ def forward(self, x, c, text):
68
+ shift1, scale1, gate1, shift2, scale2, gate2 = self.ada(c).chunk(6, dim=1)
69
+ x = x + gate1.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift1, scale1))
70
+ x = x + self.cross_gate * self.cross(self.norm_ca(x), text)
71
+ x = x + gate2.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift2, scale2))
72
+ return x
73
+
74
+ class VoxelDiT(nn.Module):
75
+ def __init__(self, vox_ch=1, vox_size=32, patch=4, dim=384, depth=12,
76
+ heads=6, text_dim=512, mlp_ratio=4.0):
77
+ super().__init__()
78
+ self.vox_ch = vox_ch
79
+ self.vox_size = vox_size
80
+ self.patch = patch
81
+ self.grid = vox_size // patch
82
+ self.patch_dim = vox_ch * patch * patch * patch
83
+ self.x_embed = nn.Linear(self.patch_dim, dim)
84
+ self.register_buffer("pos", sincos_3d(dim, self.grid).unsqueeze(0))
85
+ self.t_mlp = nn.Sequential(nn.Linear(dim, dim), nn.SiLU(), nn.Linear(dim, dim))
86
+ self.text_proj = nn.Linear(text_dim, dim)
87
+ self.text_pool = nn.Linear(text_dim, dim)
88
+ self.blocks = nn.ModuleList([Block(dim, heads, mlp_ratio) for _ in range(depth)])
89
+ self.norm_out = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
90
+ self.ada_out = nn.Sequential(nn.SiLU(), nn.Linear(dim, 2 * dim))
91
+ self.head = nn.Linear(dim, self.patch_dim)
92
+ self.dim = dim
93
+ self._init()
94
+
95
+ def _init(self):
96
+ for m in self.modules():
97
+ if isinstance(m, nn.Linear):
98
+ nn.init.xavier_uniform_(m.weight)
99
+ if m.bias is not None:
100
+ nn.init.zeros_(m.bias)
101
+ for b in self.blocks:
102
+ nn.init.zeros_(b.ada[-1].weight); nn.init.zeros_(b.ada[-1].bias)
103
+ nn.init.zeros_(self.ada_out[-1].weight); nn.init.zeros_(self.ada_out[-1].bias)
104
+ nn.init.zeros_(self.head.weight); nn.init.zeros_(self.head.bias)
105
+
106
+ def patchify(self, x):
107
+ B, C, D, H, W = x.shape
108
+ p = self.patch
109
+ x = x.reshape(B, C, D // p, p, H // p, p, W // p, p)
110
+ x = x.permute(0, 2, 4, 6, 1, 3, 5, 7)
111
+ return x.reshape(B, (D // p) * (H // p) * (W // p), C * p * p * p)
112
+
113
+ def unpatchify(self, x):
114
+ B, N, _ = x.shape
115
+ p = self.patch
116
+ g = self.grid
117
+ C = self.vox_ch
118
+ x = x.reshape(B, g, g, g, C, p, p, p)
119
+ x = x.permute(0, 4, 1, 5, 2, 6, 3, 7)
120
+ return x.reshape(B, C, g * p, g * p, g * p)
121
+
122
+ def forward(self, x, t, text_seq, text_pool):
123
+ x = self.x_embed(self.patchify(x)) + self.pos
124
+ c = self.t_mlp(timestep_embedding(t, self.dim)) + self.text_pool(text_pool)
125
+ text = self.text_proj(text_seq)
126
+ for blk in self.blocks:
127
+ x = blk(x, c, text)
128
+ shift, scale = self.ada_out(c).chunk(2, dim=1)
129
+ x = modulate(self.norm_out(x), shift, scale)
130
+ x = self.head(x)
131
+ return self.unpatchify(x)
132
+
133
+ def num_params(self):
134
+ return sum(p.numel() for p in self.parameters())