File size: 5,610 Bytes
ed66dc1
 
 
 
 
74728e5
 
 
 
 
 
 
ed66dc1
 
 
 
 
 
 
 
 
 
 
 
 
 
74728e5
 
 
 
ed66dc1
 
74728e5
 
 
ed66dc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74728e5
 
ed66dc1
 
 
74728e5
 
 
ed66dc1
74728e5
 
 
 
 
 
 
 
 
 
 
 
 
 
ed66dc1
74728e5
ed66dc1
 
 
 
 
 
74728e5
ed66dc1
74728e5
 
 
ed66dc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""models/<v>/manifest.json: every graph's signatures (input / output names and shapes, read from the flatbuffer), the
accelerator and precision it runs at, which matting graph the app uses, and the raw side files (little-endian float32) with their shapes, for the Kotlin app.
Also writes gs_perturbation.f32 next to gs_perturbation.npy.

  python make_manifest.py models/v1 [--dis PATH_TO_dis.tflite]

With den_joint_mid.tflite and den_joint_w.f32 in the folder (export_shared_joint.py) and no den_joint_01..22, the
manifest lists den_joint_mid in their place with `shared_blocks` [1..22], `block_weights` and `first_weight_input`
(the inputs before the weights), and the weights file as a side file [22, floats per block]. gs_mid.tflite and
gs_w.f32 (export_shared_gauss.py) with no gs_1..6 likewise stand for gs_1..6.

--precision fp16 runs the GPU graphs at FP16 without fp32 accumulation (the default keeps FP16_WITH_FP32_ACCUM).
"""
import argparse
import hashlib
import json
import os
import sys

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lrt import sig_io  # noqa: E402

FP32_GRAPHS = set()   # graphs whose fp16 form fails its CONTRACT.md line (none since the key-chunked attention)
CPU_GRAPHS = {'bir'}  # BiRefNet: its deformable convolutions lower to GATHER_ND, which the LiteRT GPU does not run
# A CPU graph's working memory comes with its call, after the compile: how far MemAvailable fell from just before the
# compile to the low point of one call on the Galaxy S26 (LiteRT 2.3.0 CPU; device/g2/bir3.samples.txt: 6,492,924 kB
# before, 3,760,320 kB lowest = 2,732,604 kB). The app's memory check counts it before BiRefNet runs.
RUN_PEAK_BYTES = {'bir': 2_732_604 * 1024}
ORDER = (['bir', 'dis'] + [f'dino_{i}' for i in range(4)] + ['vae_0', 'vae_1', 'vae_2', 'vae_3', 'vae_attn', 'vae_4'] +
         ['den_ctx', 'den_in'] + [f'den_joint_{k:02d}' for k in range(24)] + ['oct'] + [f'gs_{i}' for i in range(8)])
# shared graph -> (the json its export script writes, its weights file, the graph name of block k)
SHARED = {'den_joint_mid': ('shared_joint.json', 'den_joint_w.f32', lambda k: f'den_joint_{k:02d}'),
          'gs_mid': ('shared_gs.json', 'gs_w.f32', lambda k: f'gs_{k}')}
SIDE = {'den_pos_emb.f32': [1, 8192, 1024], 'dino_rope_cos.f32': [1, 1, 4101, 64], 'dino_rope_sin.f32': [1, 1, 4101, 64],
        'gs_perturbation.f32': None}


def sha256(path):
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for block in iter(lambda: f.read(1 << 24), b''):
            h.update(block)
    return h.hexdigest()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('models')
    ap.add_argument('--dis', default=None)
    ap.add_argument('--no-sha', action='store_true')
    ap.add_argument('--precision', choices=['fp16_with_fp32_accum', 'fp16'], default='fp16_with_fp32_accum',
                    help='precision of the GPU graphs')
    a = ap.parse_args()
    d = a.models
    pert = np.load(os.path.join(d, 'gs_perturbation.npy')).astype('<f4')
    pf = os.path.join(d, 'gs_perturbation.f32')
    pert.tofile(pf + '.tmp')
    os.replace(pf + '.tmp', pf)   # a new file: model folders share files by hard link
    SIDE['gs_perturbation.f32'] = list(pert.shape)
    order = list(ORDER)
    shared = {}
    for name, (js, wfile, block_name) in SHARED.items():
        if not os.path.exists(os.path.join(d, f'{name}.tflite')):
            continue
        sj = json.load(open(os.path.join(d, js)))
        mids = [block_name(k) for k in sj['blocks']]
        if os.path.exists(os.path.join(d, f'{mids[0]}.tflite')):
            continue   # the per-block graphs are there: the folder runs them
        i = order.index(mids[0])
        order = order[:i] + [name] + [n for n in order[i:] if n not in mids]
        SIDE[wfile] = [len(sj['blocks']), sj['per_block_floats']]
        shared[name] = {'shared_blocks': sj['blocks'], 'block_weights': wfile,
                        'first_weight_input': sj.get('first_weight_input', 3)}
    graphs = {}
    for name in order:
        path = a.dis if name == 'dis' else os.path.join(d, f'{name}.tflite')
        if not path or not os.path.exists(path):
            print('missing', name)
            continue
        graphs[name] = {'file': 'dis.tflite' if name == 'dis' else f'{name}.tflite', 'bytes': os.path.getsize(path),
                        'accelerator': 'cpu' if name in CPU_GRAPHS else 'gpu',
                        'precision': 'fp32' if name in FP32_GRAPHS | CPU_GRAPHS else a.precision,
                        'signatures': sig_io(path)}
        if name in RUN_PEAK_BYTES:
            graphs[name]['run_peak_bytes'] = RUN_PEAK_BYTES[name]
        graphs[name].update(shared.get(name, {}))
        if not a.no_sha:
            graphs[name]['sha256'] = sha256(path)
    side = {}
    for f, shape in SIDE.items():
        p = os.path.join(d, f)
        n = os.path.getsize(p) // 4
        assert n == int(np.prod(shape)), (f, n, shape)
        side[f] = {'shape': shape, 'dtype': 'float32_le'}
    man = {'matting': {'default': 'bir', 'alternative': 'dis'}, 'graphs': graphs, 'side_files': side, 'json_files': ['vae_bn.json', 'gs_rep.json'],
           'total_graph_bytes': sum(g['bytes'] for g in graphs.values())}
    tmp = os.path.join(d, 'manifest.json.tmp')
    with open(tmp, 'w') as f:
        json.dump(man, f, indent=1)
    os.replace(tmp, os.path.join(d, 'manifest.json'))   # atomic: runs reading the manifest never see half a file
    print(f'{len(graphs)} graphs, {man["total_graph_bytes"] / 1e9:.2f} GB')


if __name__ == '__main__':
    main()