Files
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

393 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Merge LoRA adapters into a baked SA3 DiT ``.tflite`` by patching its weight
buffers — the TFLite counterpart of the MLX weight-dict merge.
A ``.tflite`` is a frozen FlatBuffer: there is no weight dict to edit at load, so
LoRA inference means rewriting the FULLY_CONNECTED weight buffers in place. The
mechanism (validated on the 5.8 GB medium DiT):
1. Parse the FlatBuffer header (mmap, no full read) and map every
checkpoint layer name → the file offset/size/dtype of its FC weight buffer.
2. Clone the base file (APFS ``cp -c`` = a few ms and no extra disk until
written; plain copy elsewhere).
3. For each adapter layer: read W0 from the clone's mmap, compute W' with the
shared numpy merge math (``lora_core.merged_weight``), and overwrite the
buffer's bytes at its exact offset. Deltas from multiple adapters accumulate
against the ORIGINAL W0 (order-independent), matching the MLX path.
Patching happens BEFORE ``Interpreter(...)`` so XNNPACK packs the merged weights
at ``allocate_tensors`` exactly as for an unpatched model — no runtime cost, no
cache interaction. Patched files are cached by a content hash of (base, adapters,
strengths), so repeat runs reuse them instantly.
Precision: fp32 (weights are direct fp32 consts) and w16a32 (weights are fp16
consts behind a DEQUANTIZE) are supported. Quantized-int8 variants (w8a32,
w8a8-dyn, w4a32) are refused — merging requires a dequant→merge→requant that
sacrifices the GPTQ error-feedback grid (a phase-2 item).
Name mapping tiers (all hard-fail on ambiguity — never silently mis-patch):
1. Block linears (self_attn/cross_attn/ff): name carries ``TransformerBlock_N``
→ (block, module, leaf) + shape → unique.
2. ``to_local_embed.seq.{0,2}``: per-block but the tflite names carry NO block
index and their ``;NN`` counter runs BACKWARDS — block identity comes from
FC topological order within the group, never the counter.
3. Prefix embedders / project_in / project_out: singleton names → unique.
4. Seconds conditioner: the unique ``(768, 256)`` FC (in-graph, patchable).
"""
from __future__ import annotations
import hashlib
import mmap
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import lora_core as lc # noqa: E402
# Bump when the merge math or mapping changes so stale cache entries are missed.
_MERGE_VERSION = "1"
_QUANTIZED_PRECISIONS = ("w8a32", "w8a8-dyn", "w4a32")
# ── FlatBuffer FC-weight discovery ─────────────────────────────────────────────
def _fc_weight_buffers(buf):
"""Yield one record per FULLY_CONNECTED weight, in graph topological order:
``dict(name, shape, off, size, np_dtype, op_index)``.
Resolves the weight buffer through a DEQUANTIZE (w16a32: fp16 const) or takes
the FC's weight input directly (fp32: fp32 const). Refuses int8 weights.
"""
from ai_edge_litert import schema_py_generated as schema
BO = schema.BuiltinOperator
TT = schema.TensorType
_NP = {TT.FLOAT32: np.float32, TT.FLOAT16: np.float16}
m = schema.Model.GetRootAs(buf, 0)
sg = m.Subgraphs(0)
producer = {}
for oi in range(sg.OperatorsLength()):
op = sg.Operators(oi)
bc = m.OperatorCodes(op.OpcodeIndex()).BuiltinCode()
for j in range(op.OutputsLength()):
producer[op.Outputs(j)] = (bc, op)
for oi in range(sg.OperatorsLength()):
op = sg.Operators(oi)
if m.OperatorCodes(op.OpcodeIndex()).BuiltinCode() != BO.FULLY_CONNECTED:
continue
w_idx = op.Inputs(1)
wt = sg.Tensors(w_idx)
shape = tuple(wt.Shape(i) for i in range(wt.ShapeLength()))
name = (wt.Name() or b"?").decode()
prod = producer.get(w_idx)
if prod is not None and prod[0] == BO.DEQUANTIZE:
# w16a32: FC weight ← DEQUANTIZE ← fp16 const (the patch target).
src = sg.Tensors(prod[1].Inputs(0))
ttype = src.Type()
b = m.Buffers(src.Buffer())
src_name = (src.Name() or wt.Name() or b"?").decode()
else:
ttype, b, src_name = wt.Type(), m.Buffers(wt.Buffer()), name
if ttype not in _NP:
raise lc.LoraError(
f"FC weight {name!r} is {TT.__dict__ and _tt_name(ttype)} — LoRA merge "
f"on the TFLite backend supports fp32 and w16a32 (fp16) weights only. "
f"Quantized-int8 models (w8a32 / w8a8-dyn / w4a32) are not supported "
f"(merging would destroy the GPTQ grid). Use --precision fp32 or w16a32."
)
off, size = b.Offset(), b.Size()
if off == 0 or size == 0:
raise lc.LoraError(
f"FC weight {name!r} is not stored in an out-of-band buffer "
f"(off={off}, size={size}) — cannot patch in place."
)
yield dict(name=name, dbg=src_name, shape=shape, off=off, size=size,
np_dtype=_NP[ttype], op_index=oi)
def _tt_name(code):
from ai_edge_litert import schema_py_generated as schema
return next((k for k, v in schema.TensorType.__dict__.items() if v == code), str(code))
# ── name classification → checkpoint-layer resolution ──────────────────────────
_LEAF = re.compile(r"torch\.nn\.modules\.linear\.Linear_([A-Za-z0-9_]+)")
_BLOCK = re.compile(r"TransformerBlock_(\d+)/")
# checkpoint (post model.-strip) block-module suffix → (module tag, tflite leaf, glu)
_BLOCK_CKPT = {
"self_attn.to_qkv": ("self_attn", "to_qkv", False),
"self_attn.to_out": ("self_attn", "to_out", False),
"cross_attn.to_q": ("cross_attn", "to_q", False),
"cross_attn.to_kv": ("cross_attn", "to_kv", False),
"cross_attn.to_out": ("cross_attn", "to_out", False),
"ff.ff.0.proj": ("ff", "proj", True),
"ff.ff.2": ("ff", "2", False),
# to_local_embed handled separately (topological order); accept both the
# torch (.0/.2) and MLX (.seq.0/.seq.2) spellings.
"to_local_embed.0": ("local_embed", "0", False),
"to_local_embed.2": ("local_embed", "2", False),
"to_local_embed.seq.0": ("local_embed", "0", False),
"to_local_embed.seq.2": ("local_embed", "2", False),
}
# non-block singleton checkpoint names → a substring that uniquely identifies the
# tflite FC name (verified against the exported graph).
_SINGLETON_CKPT = {
"transformer.project_in": "Linear_project_in",
"transformer.project_out": "Linear_project_out",
"to_timestep_embed.0": "Sequential_to_timestep_embed/torch.nn.modules.linear.Linear_0",
"to_timestep_embed.2": "Sequential_to_timestep_embed/torch.nn.modules.linear.Linear_2",
"to_cond_embed.0": "Sequential_to_cond_embed/torch.nn.modules.linear.Linear_0",
"to_cond_embed.2": "Sequential_to_cond_embed/torch.nn.modules.linear.Linear_2",
"to_global_embed.0": "Sequential_to_global_embed/torch.nn.modules.linear.Linear_0",
"to_global_embed.2": "Sequential_to_global_embed/torch.nn.modules.linear.Linear_2",
"transformer.global_cond_embedder.0": "Sequential_global_cond_embedder/torch.nn.modules.linear.Linear_0",
"transformer.global_cond_embedder.2": "Sequential_global_cond_embedder/torch.nn.modules.linear.Linear_2",
}
def _module_tag(name: str) -> str | None:
if "SelfAttention_self_attn" in name:
return "self_attn"
if "CrossAttention_cross_attn" in name:
return "cross_attn"
if "Seq_to_local_embed" in name or "Sequential_seq" in name:
return "local_embed"
if "FeedForward_ff" in name:
return "ff"
return None
class _Mapper:
"""Resolves checkpoint layer names → FC weight records for one base model."""
def __init__(self, fcs):
self._fcs = fcs
# tier-1 block linears: (block, module, leaf, glu) → record
self._block = {}
# tier-2 local_embed: leaf ('0'/'2') → [records in topological order]
self._local = {"0": [], "2": []}
self._seconds = [] # tier-4
for rec in fcs: # fcs already in topological (op) order
name = rec["name"]
mod = _module_tag(name)
mleaf = _LEAF.search(name)
leaf = mleaf.group(1) if mleaf else None
mblk = _BLOCK.search(name)
if mod == "local_embed" and leaf in ("0", "2"):
self._local[leaf].append(rec)
elif mod in ("self_attn", "cross_attn", "ff") and mblk and leaf:
glu = "_GLUWrap_0" in name
self._block[(int(mblk.group(1)), mod, leaf, glu)] = rec
if rec["shape"] == (768, 256):
self._seconds.append(rec)
def resolve(self, layer: str, want_shape):
"""Return the FC record for a checkpoint layer, or raise LoraError."""
if layer == lc.COND_SECONDS_LAYER:
cands = [r for r in self._seconds if r["shape"] == want_shape]
return self._one(layer, want_shape, cands, "seconds conditioner")
stripped = layer[len("model."):] if layer.startswith("model.") else layer
mm = re.match(r"transformer\.layers\.(\d+)\.(.+)$", stripped)
if mm:
block, suffix = int(mm.group(1)), mm.group(2)
spec = _BLOCK_CKPT.get(suffix)
if spec is None:
raise lc.LoraError(f"{layer}: unrecognised block sub-module {suffix!r}")
module, leaf, glu = spec
if module == "local_embed":
# tier 2 — topological rank within the group is the block index.
group = self._local[leaf]
if block >= len(group):
raise lc.LoraError(
f"{layer}: to_local_embed block {block} out of range "
f"(only {len(group)} in the graph)")
rec = group[block]
if rec["shape"] != tuple(want_shape):
raise lc.LoraError(
f"{layer}: shape {rec['shape']} != adapter {tuple(want_shape)} "
f"— wrong base model?")
return rec
rec = self._block.get((block, module, leaf, glu))
cands = [rec] if rec is not None and rec["shape"] == tuple(want_shape) else []
return self._one(layer, want_shape, cands, "block linear")
sub = _SINGLETON_CKPT.get(stripped)
if sub is None:
raise lc.LoraError(f"{layer}: no TFLite tensor mapping for this layer")
cands = [r for r in self._fcs if sub in r["name"] and r["shape"] == tuple(want_shape)]
return self._one(layer, want_shape, cands, "singleton")
@staticmethod
def _one(layer, want_shape, cands, tier):
if len(cands) == 1:
return cands[0]
raise lc.LoraError(
f"{layer}: expected exactly 1 matching FC ({tier}, shape {tuple(want_shape)}), "
f"found {len(cands)} — wrong base model or unsupported target module?"
)
# ── clone + patch + cache ──────────────────────────────────────────────────────
def _clone(src: str, dst: str) -> None:
"""COW-clone src→dst (APFS ``cp -c``, instant) or fall back to a full copy."""
try:
r = subprocess.run(["cp", "-c", src, dst], capture_output=True)
if r.returncode == 0 and os.path.exists(dst):
return
except (OSError, FileNotFoundError):
pass
shutil.copyfile(src, dst)
def _cache_key(base_path: str, specs) -> str:
h = hashlib.sha256()
h.update(_MERGE_VERSION.encode())
st = os.stat(base_path)
h.update(f"{os.path.basename(base_path)}:{st.st_size}".encode())
for path, strength in sorted((lc._resolve_path(s["path"]), s["strength"]) for s in specs):
fh = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
fh.update(chunk)
h.update(f"{fh.hexdigest()}:{strength}".encode())
return h.hexdigest()[:12]
def get_patched_dit(base_path, specs, *, family: str, precision: str,
cache_dir=None, log=print) -> Path:
"""Return a path to a DiT ``.tflite`` with ``specs`` (from
:func:`lora_core.parse_lora_spec`) merged into its weights. Result is cached
by content hash; a hit returns instantly. Raises LoraError on any mapping,
shape, precision, or trust-boundary failure — never mis-patches silently.
"""
base_path = str(base_path)
if precision in _QUANTIZED_PRECISIONS:
raise lc.LoraError(
f"--lora is not supported with --precision {precision} (quantized-int8) — "
f"merging int8 weights would destroy the GPTQ grid. Use fp32 or w16a32."
)
# Cache next to the family dirs (models/tflite/lora_cache/) — use the
# base's OWN path, not resolve(): the base is usually a symlink into the HF
# cache, and resolving it would drop the patched (multi-GB) clones there.
cache_dir = Path(cache_dir or (Path(base_path).parent.parent / "lora_cache"))
cache_dir.mkdir(parents=True, exist_ok=True)
key = _cache_key(base_path, specs)
out = cache_dir / f"dit_{family}_{precision}_{key}.tflite"
if out.exists():
log(f"lora: reusing cached patched model {out.name}")
return out
# Parse all adapters up front (fail fast before the clone).
parsed = []
for s in specs:
path = lc._resolve_path(s["path"])
adapter_type, scaling, layers = lc.parse_adapter(path)
parsed.append((os.path.basename(path), adapter_type, scaling,
float(s["strength"]), layers))
log(f"lora: {os.path.basename(path)} — {adapter_type}, "
f"scaling {scaling:g}, strength {s['strength']:g}, {len(layers)} layers")
# Map every adapter layer against the base model BEFORE cloning (so a
# wrong-base adapter errors without leaving a stale clone behind).
with open(base_path, "rb") as f:
buf = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
fcs = list(_fc_weight_buffers(buf))
mapper = _Mapper(fcs)
# accum[off] = [record, W0(fp32), summed_delta(fp32)]
accum = {}
matched = 0
for name, atype, scaling, strength, layers in parsed:
need = lc._PARAMS_FOR.get(atype, ())
for layer, params in layers.items():
want = (params.get("lora_B", params.get("M_xs")).shape[0],
params["lora_A"].shape[1] if "lora_A" in params
else params["M_xs"].shape[1])
# For -xs, fan_in comes from the base weight; resolve then re-check.
rec = mapper.resolve(layer, want if atype not in
("lora-xs", "dora-rows-xs", "dora-cols-xs", "bora-xs")
else _shape_from_leaf(mapper, layer, params))
W0 = np.frombuffer(buf, rec["np_dtype"],
count=rec["size"] // np.dtype(rec["np_dtype"]).itemsize,
offset=rec["off"]).reshape(rec["shape"]).astype(np.float32)
missing = [n for n in need if n not in params]
if missing:
raise lc.LoraError(f"{layer}: adapter is {atype} but missing {missing}")
lc.check_shapes(layer, W0, params, atype)
merged = lc.merged_weight(W0, params, atype, scaling)
delta = strength * (merged - W0)
if rec["off"] in accum:
accum[rec["off"]][2] += delta
else:
accum[rec["off"]] = [rec, W0, delta]
matched += 1
buf.close()
if not matched:
raise lc.LoraError("no adapter layers matched this DiT (wrong base model?)")
log(f"lora: mapped {len(accum)} weight tensor(s) across {matched} adapter layer(s)")
# Clone, then patch each buffer once (W0 + summed delta).
tmp = out.with_suffix(".tflite.tmp")
if tmp.exists():
tmp.unlink()
_clone(base_path, str(tmp))
n_bytes = 0
with open(tmp, "r+b") as fw:
for off, (rec, W0, delta) in accum.items():
Wp = (W0 + delta).astype(rec["np_dtype"])
b = Wp.tobytes()
assert len(b) == rec["size"], (rec["name"], len(b), rec["size"])
fw.seek(off)
fw.write(b)
n_bytes += rec["size"]
fw.flush()
os.fsync(fw.fileno())
os.replace(tmp, out)
log(f"lora: patched {n_bytes / 1e6:.0f} MB → {out.name}")
return out
def _shape_from_leaf(mapper: _Mapper, layer: str, params: dict):
"""For -xs adapters the fan_in isn't in the params (only M_xs r×r), so map by
a shape probe: try each FC whose descriptor matches and return its shape.
Resolves by re-using the mapper's block/singleton/seconds index with a
wildcard shape, then reads the found record's shape."""
# Cheap approach: the -xs magnitude (rows/cols) or M_xs can't give fan_in, so
# locate the record ignoring shape via a temporary relaxation.
stripped = layer[len("model."):] if layer.startswith("model.") else layer
if layer == lc.COND_SECONDS_LAYER and mapper._seconds:
return mapper._seconds[0]["shape"]
mm = re.match(r"transformer\.layers\.(\d+)\.(.+)$", stripped)
if mm:
block, suffix = int(mm.group(1)), mm.group(2)
spec = _BLOCK_CKPT.get(suffix)
if spec:
module, leaf, glu = spec
if module == "local_embed":
grp = mapper._local[leaf]
if block < len(grp):
return grp[block]["shape"]
else:
rec = mapper._block.get((block, module, leaf, glu))
if rec:
return rec["shape"]
sub = _SINGLETON_CKPT.get(stripped)
if sub:
for r in mapper._fcs:
if sub in r["name"]:
return r["shape"]
raise lc.LoraError(f"{layer}: cannot locate a base weight for this -xs layer")