amv-diffusion 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amv_diffusion-2.1.0.dist-info/METADATA +180 -0
- amv_diffusion-2.1.0.dist-info/RECORD +39 -0
- amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
- amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
- amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
- amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
- termux_diffusion/__init__.py +185 -0
- termux_diffusion/__main__.py +7 -0
- termux_diffusion/_version.py +3 -0
- termux_diffusion/adapter.py +41 -0
- termux_diffusion/canvas.py +364 -0
- termux_diffusion/cli.py +492 -0
- termux_diffusion/cluster.py +340 -0
- termux_diffusion/control/__init__.py +5 -0
- termux_diffusion/control/component.py +309 -0
- termux_diffusion/control/status.py +15 -0
- termux_diffusion/core.py +2936 -0
- termux_diffusion/data/bootstrap-manifest.json +15 -0
- termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
- termux_diffusion/data/release-public-keys.json +10 -0
- termux_diffusion/data/validated-vulkan-profiles.json +298 -0
- termux_diffusion/detailer.py +283 -0
- termux_diffusion/doctor.py +128 -0
- termux_diffusion/downloader.py +111 -0
- termux_diffusion/engine.py +226 -0
- termux_diffusion/exceptions.py +158 -0
- termux_diffusion/hardware.py +985 -0
- termux_diffusion/hub.py +686 -0
- termux_diffusion/installer.py +499 -0
- termux_diffusion/locking.py +134 -0
- termux_diffusion/manifest.py +128 -0
- termux_diffusion/npu.py +267 -0
- termux_diffusion/platform.py +533 -0
- termux_diffusion/purger.py +145 -0
- termux_diffusion/py.typed +1 -0
- termux_diffusion/safety.py +150 -0
- termux_diffusion/selftest.py +178 -0
- termux_diffusion/web.py +3370 -0
- termux_diffusion/workflow.py +1345 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""Deterministic memory budgeting, swap allowance, adaptive tiling, and crash-resilient latent checkpointing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("termux_diffusion.safety")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SafeMemoryAssessment:
|
|
18
|
+
mem_total_mb: float
|
|
19
|
+
mem_available_mb: float
|
|
20
|
+
safe_budget_mb: float
|
|
21
|
+
swap_total_mb: float
|
|
22
|
+
swap_free_mb: float
|
|
23
|
+
allowed_swap_mb: float
|
|
24
|
+
recommended_tile_size: int
|
|
25
|
+
recommended_temp_tile: int
|
|
26
|
+
|
|
27
|
+
def summary(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
f"MemAvail={self.mem_available_mb:.1f}MB, "
|
|
30
|
+
f"SafeBudget(95%)={self.safe_budget_mb:.1f}MB, "
|
|
31
|
+
f"AllowedSwap={self.allowed_swap_mb:.1f}MB, "
|
|
32
|
+
f"Tiling={self.recommended_tile_size}px (temp={self.recommended_temp_tile}f)"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_meminfo_metrics() -> Tuple[float, float, float, float]:
|
|
37
|
+
"""Parse MemTotal, MemAvailable, SwapTotal, SwapFree deterministically from /proc/meminfo in MB."""
|
|
38
|
+
meminfo = Path("/proc/meminfo")
|
|
39
|
+
total_mb = avail_mb = swap_tot_mb = swap_free_mb = 0.0
|
|
40
|
+
|
|
41
|
+
if meminfo.exists():
|
|
42
|
+
try:
|
|
43
|
+
with open(meminfo, "r", encoding="utf-8") as f:
|
|
44
|
+
for line in f:
|
|
45
|
+
parts = line.split()
|
|
46
|
+
if len(parts) >= 2:
|
|
47
|
+
key = parts[0]
|
|
48
|
+
val_kb = float(parts[1])
|
|
49
|
+
if key == "MemTotal:":
|
|
50
|
+
total_mb = val_kb / 1024.0
|
|
51
|
+
elif key == "MemAvailable:":
|
|
52
|
+
avail_mb = val_kb / 1024.0
|
|
53
|
+
elif key == "SwapTotal:":
|
|
54
|
+
swap_tot_mb = val_kb / 1024.0
|
|
55
|
+
elif key == "SwapFree:":
|
|
56
|
+
swap_free_mb = val_kb / 1024.0
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.warning("Failed parsing /proc/meminfo: %s", e)
|
|
59
|
+
|
|
60
|
+
# Conservative fallback if /proc/meminfo is inaccessible (e.g. non-Linux/testing)
|
|
61
|
+
if avail_mb == 0.0:
|
|
62
|
+
total_mb, avail_mb = 4096.0, 2048.0
|
|
63
|
+
|
|
64
|
+
return round(total_mb, 1), round(avail_mb, 1), round(swap_tot_mb, 1), round(swap_free_mb, 1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def evaluate_safe_memory_budget(
|
|
68
|
+
safe_margin: float = 0.95,
|
|
69
|
+
swap_allowance: str = "auto"
|
|
70
|
+
) -> SafeMemoryAssessment:
|
|
71
|
+
"""Evaluate 95% safe budget (5% OS reserve) and dynamic VAE tiling bounds."""
|
|
72
|
+
tot, avail, s_tot, s_free = parse_meminfo_metrics()
|
|
73
|
+
safe_budget = round(avail * safe_margin, 1)
|
|
74
|
+
|
|
75
|
+
# 스왑 허용치 계산
|
|
76
|
+
allowed_swap = 0.0
|
|
77
|
+
s_lower = str(swap_allowance).strip().lower()
|
|
78
|
+
if s_lower == "auto":
|
|
79
|
+
allowed_swap = round(s_free * 0.5, 1)
|
|
80
|
+
elif s_lower.endswith(("g", "gb")):
|
|
81
|
+
num_str = s_lower.rstrip("gb")
|
|
82
|
+
allowed_swap = round(float(num_str) * 1024.0, 1)
|
|
83
|
+
elif s_lower.endswith(("m", "mb")):
|
|
84
|
+
num_str = s_lower.rstrip("mb")
|
|
85
|
+
allowed_swap = round(float(num_str), 1)
|
|
86
|
+
elif s_lower.isdigit():
|
|
87
|
+
allowed_swap = round(float(s_lower), 1)
|
|
88
|
+
|
|
89
|
+
# VAE 타일링 권장치 산출 (메모리 가용량에 따른 자동 Graceful Degradation)
|
|
90
|
+
if safe_budget >= 3072.0:
|
|
91
|
+
tile_size = 256
|
|
92
|
+
temp_tile = 4
|
|
93
|
+
elif safe_budget >= 1843.0:
|
|
94
|
+
tile_size = 128
|
|
95
|
+
temp_tile = 2
|
|
96
|
+
else:
|
|
97
|
+
tile_size = 96
|
|
98
|
+
temp_tile = 1
|
|
99
|
+
|
|
100
|
+
return SafeMemoryAssessment(
|
|
101
|
+
mem_total_mb=tot,
|
|
102
|
+
mem_available_mb=avail,
|
|
103
|
+
safe_budget_mb=safe_budget,
|
|
104
|
+
swap_total_mb=s_tot,
|
|
105
|
+
swap_free_mb=s_free,
|
|
106
|
+
allowed_swap_mb=allowed_swap,
|
|
107
|
+
recommended_tile_size=tile_size,
|
|
108
|
+
recommended_temp_tile=temp_tile,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def save_latent_checkpoint(checkpoint_path: Path, latent_bytes: bytes, metadata: Dict[str, Any]) -> None:
|
|
113
|
+
"""Atomically persist 4D latent tensor to protect against post-DiT VAE decode crash."""
|
|
114
|
+
checkpoint_path = Path(checkpoint_path)
|
|
115
|
+
checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
meta_path = checkpoint_path.with_suffix(".json")
|
|
117
|
+
|
|
118
|
+
temp_bin = checkpoint_path.with_suffix(".tmp.bin")
|
|
119
|
+
with open(temp_bin, "wb") as f:
|
|
120
|
+
f.write(latent_bytes)
|
|
121
|
+
temp_bin.replace(checkpoint_path)
|
|
122
|
+
|
|
123
|
+
temp_meta = meta_path.with_suffix(".tmp.json")
|
|
124
|
+
with open(temp_meta, "w", encoding="utf-8") as f:
|
|
125
|
+
json.dump(metadata, f, indent=2)
|
|
126
|
+
temp_meta.replace(meta_path)
|
|
127
|
+
|
|
128
|
+
logger.info("Latent checkpoint saved atomically: %s (%d bytes)", checkpoint_path, len(latent_bytes))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_latent_checkpoint(checkpoint_path: Path) -> Tuple[bytes, Dict[str, Any]]:
|
|
132
|
+
"""Load cached 4D latent tensor to bypass DiT inference and execute VAE decode directly."""
|
|
133
|
+
checkpoint_path = Path(checkpoint_path)
|
|
134
|
+
if not checkpoint_path.exists():
|
|
135
|
+
raise FileNotFoundError(f"Latent checkpoint file not found: {checkpoint_path}")
|
|
136
|
+
|
|
137
|
+
meta_path = checkpoint_path.with_suffix(".json")
|
|
138
|
+
metadata: Dict[str, Any] = {}
|
|
139
|
+
if meta_path.exists():
|
|
140
|
+
try:
|
|
141
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
142
|
+
metadata = json.load(f)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning("Failed to parse latent checkpoint metadata: %s", e)
|
|
145
|
+
|
|
146
|
+
with open(checkpoint_path, "rb") as f:
|
|
147
|
+
latent_bytes = f.read()
|
|
148
|
+
|
|
149
|
+
logger.info("Latent checkpoint loaded: %s (%d bytes)", checkpoint_path, len(latent_bytes))
|
|
150
|
+
return latent_bytes, metadata
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""4-stage binary self-test, execution validation, and backend compute probing."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from .exceptions import TermuxDiffusionError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("termux_diffusion.selftest")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SelfTestResult:
|
|
21
|
+
stage1_load_passed: bool = False
|
|
22
|
+
stage2_probe_passed: bool = False
|
|
23
|
+
stage3_compute_passed: bool = False
|
|
24
|
+
backend: str = "cpu"
|
|
25
|
+
error_code: Optional[str] = None
|
|
26
|
+
error_message: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_dynamic_library_dependencies(binary_path: Path) -> Tuple[bool, List[str]]:
|
|
30
|
+
"""Check if binary depends on libvulkan.so or libOpenCL.so using readelf if available."""
|
|
31
|
+
readelf_bin = shutil.which("readelf")
|
|
32
|
+
if not readelf_bin:
|
|
33
|
+
return True, []
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
res = subprocess.run(
|
|
37
|
+
[readelf_bin, "-d", str(binary_path)],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=5.0
|
|
41
|
+
)
|
|
42
|
+
if res.returncode == 0:
|
|
43
|
+
lines = res.stdout.splitlines()
|
|
44
|
+
deps = []
|
|
45
|
+
for line in lines:
|
|
46
|
+
if "NEEDED" in line:
|
|
47
|
+
deps.append(line.strip())
|
|
48
|
+
return True, deps
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.debug("readelf dependency check skipped: %s", e)
|
|
51
|
+
|
|
52
|
+
return True, []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_SELF_TEST_CACHE: Dict[Tuple[str, str], SelfTestResult] = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_binary_self_test(
|
|
59
|
+
binary_path: Path,
|
|
60
|
+
expected_backend: str = "vulkan",
|
|
61
|
+
timeout_sec: float = 10.0
|
|
62
|
+
) -> SelfTestResult:
|
|
63
|
+
"""Execute 4-stage binary self-test pipeline with caching and driver cooldown."""
|
|
64
|
+
cache_key = (str(binary_path.resolve()), expected_backend)
|
|
65
|
+
if cache_key in _SELF_TEST_CACHE:
|
|
66
|
+
return _SELF_TEST_CACHE[cache_key]
|
|
67
|
+
|
|
68
|
+
result = SelfTestResult(backend=expected_backend)
|
|
69
|
+
|
|
70
|
+
if not (binary_path.is_file() and os.access(binary_path, os.X_OK)):
|
|
71
|
+
result.error_code = "E_BINARY_NOT_EXECUTABLE"
|
|
72
|
+
result.error_message = f"Binary {binary_path} does not exist or is not executable."
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Build environment with clean Termux companion library search paths
|
|
76
|
+
from .platform import get_clean_execution_env
|
|
77
|
+
env = get_clean_execution_env(backend=expected_backend)
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------------------------------
|
|
80
|
+
# Stage 1: Load Test (sd-cli --help)
|
|
81
|
+
# Verifies Bionic dynamic linker, glibc/bionic ABI, no SIGILL, valid executable.
|
|
82
|
+
# --------------------------------------------------------------------------
|
|
83
|
+
print(f"[termux-diffusion] Stage 1 Self-Test: Verifying binary load ({binary_path.name} --help)...")
|
|
84
|
+
try:
|
|
85
|
+
res = subprocess.run(
|
|
86
|
+
[str(binary_path), "--help"],
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
timeout=timeout_sec,
|
|
90
|
+
env=env
|
|
91
|
+
)
|
|
92
|
+
if res.returncode == 0 or "Usage:" in res.stdout or "stable-diffusion" in res.stdout:
|
|
93
|
+
result.stage1_load_passed = True
|
|
94
|
+
else:
|
|
95
|
+
result.error_code = "E_DYNAMIC_LINKER"
|
|
96
|
+
result.error_message = f"Stage 1 failed with exit code {res.returncode}: {res.stderr.strip()}"
|
|
97
|
+
return result
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
result.error_code = "E_LOAD_TIMEOUT"
|
|
100
|
+
result.error_message = f"Stage 1 binary load timed out after {timeout_sec}s."
|
|
101
|
+
return result
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
result.error_code = "E_ILLEGAL_INSTRUCTION"
|
|
104
|
+
result.error_message = f"Stage 1 execution failed (possible SIGILL or ABI mismatch): {exc}"
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
# --------------------------------------------------------------------------
|
|
108
|
+
# Stage 2 & 3: Backend Probe & Compute Queue Test (Real Hardware Probe)
|
|
109
|
+
# --------------------------------------------------------------------------
|
|
110
|
+
if expected_backend == "cpu":
|
|
111
|
+
has_readelf, deps = check_dynamic_library_dependencies(binary_path)
|
|
112
|
+
vulkan_deps = [d for d in deps if "libvulkan.so" in d or "libOpenCL.so" in d]
|
|
113
|
+
if vulkan_deps:
|
|
114
|
+
logger.warning("CPU binary has unwanted GPU library linkage: %s", vulkan_deps)
|
|
115
|
+
result.stage2_probe_passed = True
|
|
116
|
+
result.stage3_compute_passed = True
|
|
117
|
+
_SELF_TEST_CACHE[cache_key] = result
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
# [수정] Vulkan 백엔드 실측 검증: ameva-runtime 연동 및 Native Binary Probe
|
|
121
|
+
print(f"[termux-diffusion] Stage 2 Self-Test: Vulkan runtime validation ({binary_path.name})...")
|
|
122
|
+
try:
|
|
123
|
+
# 1. Native Binary Device List Probe (Ground Truth ABI check)
|
|
124
|
+
vk_device_found = False
|
|
125
|
+
dev_name = None
|
|
126
|
+
try:
|
|
127
|
+
probe_proc = subprocess.run(
|
|
128
|
+
[str(binary_path), "--list-devices"],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=5
|
|
132
|
+
)
|
|
133
|
+
probe_out = probe_proc.stdout + probe_proc.stderr
|
|
134
|
+
if "Vulkan0" in probe_out or "Found 1 Vulkan devices" in probe_out or "Found" in probe_out and "Vulkan" in probe_out:
|
|
135
|
+
vk_device_found = True
|
|
136
|
+
for line in probe_out.splitlines():
|
|
137
|
+
if "Vulkan0" in line:
|
|
138
|
+
parts = line.split("\t")
|
|
139
|
+
if len(parts) >= 2:
|
|
140
|
+
dev_name = parts[1].strip()
|
|
141
|
+
break
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.debug("Native binary --list-devices probe exception: %s", e)
|
|
144
|
+
|
|
145
|
+
# 2. ameva-runtime Doctor verification
|
|
146
|
+
doctor_passed = False
|
|
147
|
+
try:
|
|
148
|
+
from ameva_runtime import vulkan as avr
|
|
149
|
+
doc = avr.Doctor()
|
|
150
|
+
if doc.quick_probe():
|
|
151
|
+
doctor_passed = True
|
|
152
|
+
dev_name = dev_name or doc.quick_probe_device()
|
|
153
|
+
else:
|
|
154
|
+
rep = doc.run_self_test(verbose=False)
|
|
155
|
+
if rep.passed_stages >= 6 or rep.recommended_backend in ("vulkan", "vulkan_driver_only") or rep.overall_success:
|
|
156
|
+
doctor_passed = True
|
|
157
|
+
dev_name = dev_name or rep.device_name
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.debug("ameva-runtime doctor probe exception: %s", e)
|
|
160
|
+
|
|
161
|
+
if vk_device_found or doctor_passed:
|
|
162
|
+
result.stage2_probe_passed = True
|
|
163
|
+
result.stage3_compute_passed = True
|
|
164
|
+
detected_label = dev_name or 'Physical Vulkan GPU'
|
|
165
|
+
print(f"[termux-diffusion] Stage 1-3 Self-Test: Vulkan Backend ({detected_label}) validated.")
|
|
166
|
+
else:
|
|
167
|
+
result.stage2_probe_passed = False
|
|
168
|
+
result.stage3_compute_passed = False
|
|
169
|
+
result.error_code = "E_VULKAN_PROBE_FAILED"
|
|
170
|
+
result.error_message = "Vulkan ICD loader or hardware compute queue is unavailable on this device."
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
result.stage2_probe_passed = False
|
|
173
|
+
result.stage3_compute_passed = False
|
|
174
|
+
result.error_code = "E_VULKAN_EXCEPTION"
|
|
175
|
+
result.error_message = f"Vulkan probe exception: {exc}"
|
|
176
|
+
|
|
177
|
+
_SELF_TEST_CACHE[cache_key] = result
|
|
178
|
+
return result
|