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.
Files changed (39) hide show
  1. amv_diffusion-2.1.0.dist-info/METADATA +180 -0
  2. amv_diffusion-2.1.0.dist-info/RECORD +39 -0
  3. amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
  4. amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
  5. amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
  6. amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
  7. termux_diffusion/__init__.py +185 -0
  8. termux_diffusion/__main__.py +7 -0
  9. termux_diffusion/_version.py +3 -0
  10. termux_diffusion/adapter.py +41 -0
  11. termux_diffusion/canvas.py +364 -0
  12. termux_diffusion/cli.py +492 -0
  13. termux_diffusion/cluster.py +340 -0
  14. termux_diffusion/control/__init__.py +5 -0
  15. termux_diffusion/control/component.py +309 -0
  16. termux_diffusion/control/status.py +15 -0
  17. termux_diffusion/core.py +2936 -0
  18. termux_diffusion/data/bootstrap-manifest.json +15 -0
  19. termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
  20. termux_diffusion/data/release-public-keys.json +10 -0
  21. termux_diffusion/data/validated-vulkan-profiles.json +298 -0
  22. termux_diffusion/detailer.py +283 -0
  23. termux_diffusion/doctor.py +128 -0
  24. termux_diffusion/downloader.py +111 -0
  25. termux_diffusion/engine.py +226 -0
  26. termux_diffusion/exceptions.py +158 -0
  27. termux_diffusion/hardware.py +985 -0
  28. termux_diffusion/hub.py +686 -0
  29. termux_diffusion/installer.py +499 -0
  30. termux_diffusion/locking.py +134 -0
  31. termux_diffusion/manifest.py +128 -0
  32. termux_diffusion/npu.py +267 -0
  33. termux_diffusion/platform.py +533 -0
  34. termux_diffusion/purger.py +145 -0
  35. termux_diffusion/py.typed +1 -0
  36. termux_diffusion/safety.py +150 -0
  37. termux_diffusion/selftest.py +178 -0
  38. termux_diffusion/web.py +3370 -0
  39. termux_diffusion/workflow.py +1345 -0
@@ -0,0 +1,985 @@
1
+ # Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0 OR Commercial-License
3
+ """Hardware acceleration detection, Vulkan/OpenCL/NPU/TPU probing, and CMake flag generation.
4
+
5
+ This module probes the Android device for available GPU, NPU, TPU, and CPU compute backends
6
+ by checking for the actual existence of system driver libraries on disk.
7
+ """
8
+
9
+ import ctypes
10
+ import logging
11
+ import os
12
+ import platform
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional, Tuple, Any
22
+
23
+ from .exceptions import PlatformNotSupportedError, RuntimeRequiredError
24
+ from .npu import NPUProfile, NPUVendor, detect_npu_capabilities, get_optimal_heterogeneous_pipeline
25
+
26
+ logger = logging.getLogger("termux_diffusion.hardware")
27
+
28
+
29
+ class ComputeBackend(Enum):
30
+ """Available hardware compute backends, ordered by expected throughput."""
31
+ CPU_NEON = "cpu"
32
+ OPENCL = "opencl"
33
+ VULKAN = "vulkan"
34
+ NPU = "npu"
35
+ TPU = "tpu"
36
+
37
+
38
+ @dataclass
39
+ class GPUDriverInfo:
40
+ """Detected GPU driver metadata from the device."""
41
+ name: str = "Unknown"
42
+ vendor: str = "Unknown"
43
+ api: str = "Unknown"
44
+ library_path: str = ""
45
+ version: str = ""
46
+ usable: bool = False
47
+
48
+
49
+ @dataclass
50
+ class HardwareProfile:
51
+ """Complete hardware capability profile for the current device."""
52
+ cpu_arch: str = ""
53
+ cpu_cores: int = 0
54
+ cpu_features: List[str] = field(default_factory=list)
55
+ has_dotprod: bool = False
56
+ has_fp16: bool = False
57
+ has_i8mm: bool = False
58
+ has_sve: bool = False
59
+ vulkan_available: bool = False
60
+ vulkan_driver: Optional[GPUDriverInfo] = None
61
+ opencl_available: bool = False
62
+ opencl_driver: Optional[GPUDriverInfo] = None
63
+ npu_profile: Optional[NPUProfile] = None
64
+ soc_name: str = "Unknown"
65
+ gpu_name: str = "Unknown"
66
+ recommended_backend: ComputeBackend = ComputeBackend.CPU_NEON
67
+ recommended_ngl: int = 0
68
+ cmake_extra_flags: List[str] = field(default_factory=list)
69
+
70
+ @property
71
+ def soc_model(self) -> str:
72
+ return self.soc_name
73
+
74
+ @property
75
+ def cpu_count(self) -> int:
76
+ return self.cpu_cores
77
+
78
+ @property
79
+ def recommended_threads(self) -> int:
80
+ return max(1, self.cpu_cores // 2) if self.cpu_cores > 2 else self.cpu_cores
81
+
82
+ @property
83
+ def threads(self) -> int:
84
+ return self.recommended_threads
85
+
86
+
87
+ # ------------------------------------------------------------------------------
88
+ # 1. CPU & GPU Feature Detection
89
+ # ------------------------------------------------------------------------------
90
+
91
+ # [중요] Vulkan ICD 탐색 순서: Android Bionic ICD 최우선.
92
+ # Termux Mesa($PREFIX/lib/libvulkan.so)를 시스템 ICD보다 먼저 로드하면
93
+ # Bionic linker 이중 dispatch 테이블 충돌로 SIGABRT 가 발생합니다.
94
+ # ameva-runtime 의 ICD 탐색 정책과 동일하게 유지합니다.
95
+ _VULKAN_LIB_SEARCH_PATHS = [
96
+ "/system/lib64/libvulkan.so", # Android Bionic ICD (최우선 — A35/S25/S21 모두 존재)
97
+ "/system/lib/libvulkan.so",
98
+ "/vendor/lib64/libvulkan.so",
99
+ "/vendor/lib/libvulkan.so",
100
+ # Termux Mesa: 시스템 ICD 가 없는 순수 Linux/PRoot 환경 전용 fallback
101
+ os.path.join(os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "lib64", "libvulkan.so"),
102
+ os.path.join(os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "lib", "libvulkan.so"),
103
+ ]
104
+
105
+ _OPENCL_LIB_SEARCH_PATHS = [
106
+ "/vendor/lib64/libOpenCL.so",
107
+ "/system/lib64/libOpenCL.so",
108
+ "/system/vendor/lib64/libOpenCL.so",
109
+ "/vendor/lib/libOpenCL.so",
110
+ "/system/lib/libOpenCL.so",
111
+ "/vendor/lib64/egl/libGLES_mali.so",
112
+ "/system/vendor/lib64/egl/libGLES_mali.so",
113
+ ]
114
+
115
+
116
+ def _find_vulkan_driver_path() -> Optional[str]:
117
+ """Find the first genuinely accessible Vulkan driver library on the filesystem."""
118
+ for p in _VULKAN_LIB_SEARCH_PATHS:
119
+ p_obj = Path(p)
120
+ try:
121
+ if p_obj.is_file() and p_obj.stat().st_size >= 1024:
122
+ return str(p_obj.resolve())
123
+ except (OSError, PermissionError):
124
+ continue
125
+ return None
126
+
127
+
128
+ def _read_cpuinfo_features() -> List[str]:
129
+ """Parse /proc/cpuinfo to extract ARM CPU feature flags."""
130
+ cpuinfo_path = Path("/proc/cpuinfo")
131
+ if not cpuinfo_path.exists():
132
+ return []
133
+
134
+ try:
135
+ text = cpuinfo_path.read_text(encoding="utf-8", errors="ignore")
136
+ for line in text.splitlines():
137
+ if line.lower().startswith("features"):
138
+ parts = line.split(":", 1)
139
+ if len(parts) == 2:
140
+ return parts[1].strip().split()
141
+ return []
142
+ except Exception as e:
143
+ logger.debug("Could not read /proc/cpuinfo: %s", e)
144
+ return []
145
+
146
+
147
+ from .platform import is_android_termux
148
+
149
+
150
+ def _detect_soc_name() -> str:
151
+ """Identify the SoC model from Android system properties."""
152
+ if not is_android_termux():
153
+ return "Unknown"
154
+ prop_keys = [
155
+ "ro.soc.model",
156
+ "ro.chipname",
157
+ "ro.hardware.chipname",
158
+ "ro.board.platform",
159
+ "ro.hardware",
160
+ "ro.product.board",
161
+ ]
162
+ for key in prop_keys:
163
+ try:
164
+ result = subprocess.run(
165
+ ["getprop", key],
166
+ capture_output=True,
167
+ text=True,
168
+ timeout=2.0,
169
+ )
170
+ val = result.stdout.strip()
171
+ if val and val != "unknown":
172
+ return val
173
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
174
+ continue
175
+ return "Unknown"
176
+
177
+
178
+ def _detect_device_model() -> str:
179
+ """Identify Android device model for hardware profile matching."""
180
+ if not is_android_termux():
181
+ return "Unknown"
182
+ for prop in ["ro.product.model", "ro.product.device", "ro.build.product"]:
183
+ try:
184
+ result = subprocess.run(
185
+ ["getprop", prop],
186
+ capture_output=True,
187
+ text=True,
188
+ timeout=2.0,
189
+ )
190
+ val = result.stdout.strip()
191
+ if val and val != "unknown":
192
+ return val
193
+ except Exception:
194
+ continue
195
+ return "Unknown"
196
+
197
+
198
+ def _detect_gpu_name() -> str:
199
+ """Identify the GPU model from Android properties and hardware nodes."""
200
+ if not is_android_termux():
201
+ return "Unknown"
202
+ # 1. Check Adreno kgsl sysfs node
203
+ kgsl_model = Path("/sys/class/kgsl/kgsl-3d0/gpu_model")
204
+ if kgsl_model.exists():
205
+ try:
206
+ val = kgsl_model.read_text(encoding="utf-8").strip()
207
+ if val:
208
+ return f"Adreno ({val})"
209
+ except PermissionError as _perm_err:
210
+ logger.warning("[hardware] kgsl gpu_model read PermissionError: %s", _perm_err)
211
+ except OSError as _os_err:
212
+ logger.warning("[hardware] kgsl gpu_model read OSError: %s", _os_err)
213
+ # 예상 밖 예외는 재발생
214
+
215
+ # 2. Check Android system properties
216
+ for prop in ["ro.hardware.vulkan", "ro.hardware.egl", "ro.board.platform"]:
217
+ try:
218
+ result = subprocess.run(
219
+ ["getprop", prop],
220
+ capture_output=True,
221
+ text=True,
222
+ timeout=2.0,
223
+ )
224
+ val = result.stdout.strip()
225
+ if val and val != "unknown":
226
+ if "adreno" in val.lower():
227
+ return f"Qualcomm Adreno ({val})"
228
+ if "mali" in val.lower():
229
+ return f"ARM Mali ({val})"
230
+ return val.capitalize()
231
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
232
+ continue
233
+ return "Unknown"
234
+
235
+
236
+
237
+
238
+ def _probe_vulkan_driver() -> Optional[GPUDriverInfo]:
239
+ """Probe for a usable Vulkan driver via ameva-runtime SSOT."""
240
+ try:
241
+ from ameva_runtime import vulkan as avr
242
+ report = avr.Doctor().run_self_test(verbose=False)
243
+ if report.overall_success or report.recommended_backend in ("vulkan", "vulkan_driver_only") or getattr(report, "passed_stages", 0) >= 7:
244
+ return GPUDriverInfo(
245
+ name=f"Vulkan Driver ({report.device_name})",
246
+ vendor=report.device_name,
247
+ api=f"Vulkan {getattr(report, 'driver_version', '1.3')}",
248
+ library_path=getattr(report, "loader_path", "") or _find_vulkan_driver_path() or "/system/lib64/libvulkan.so",
249
+ version=getattr(report, "driver_version", ""),
250
+ usable=True,
251
+ )
252
+ except Exception as e:
253
+ logger.debug("[termux-diffusion] ameva-runtime probe exception: %s", e)
254
+
255
+ # 안전 폴백: 시스템 기본 Bionic 경로 검사
256
+ detected_path = _find_vulkan_driver_path()
257
+ if detected_path and Path(detected_path).is_file():
258
+ gpu_name = _detect_gpu_name()
259
+ return GPUDriverInfo(
260
+ name=f"Vulkan Driver ({gpu_name})",
261
+ vendor=gpu_name,
262
+ api="Vulkan",
263
+ library_path=detected_path,
264
+ version="",
265
+ usable=True,
266
+ )
267
+ return None
268
+
269
+
270
+ def _probe_opencl_driver() -> Optional[GPUDriverInfo]:
271
+ """Probe for a usable OpenCL driver by checking library paths."""
272
+ for lib_path in _OPENCL_LIB_SEARCH_PATHS:
273
+ p = Path(lib_path)
274
+ if p.is_file():
275
+ try:
276
+ size = p.stat().st_size
277
+ if size < 1024:
278
+ continue
279
+ gpu_name = _detect_gpu_name()
280
+ return GPUDriverInfo(
281
+ name=f"OpenCL Driver ({gpu_name})",
282
+ vendor=gpu_name,
283
+ api="OpenCL",
284
+ library_path=lib_path,
285
+ version="",
286
+ usable=True,
287
+ )
288
+ except PermissionError as pe:
289
+ logger.warning(
290
+ "OpenCL driver found at '%s' but access was denied (SELinux permission): %s",
291
+ lib_path,
292
+ pe,
293
+ )
294
+ continue
295
+ except OSError as oe:
296
+ logger.debug("OpenCL driver stat note on '%s': %s", lib_path, oe)
297
+ continue
298
+ return None
299
+
300
+
301
+ # ------------------------------------------------------------------------------
302
+ # 2. Main Hardware Profiler
303
+ # ------------------------------------------------------------------------------
304
+
305
+ def detect_hardware_profile() -> HardwareProfile:
306
+ """Run comprehensive hardware detection across CPU, GPU, NPU, and TPU."""
307
+ profile = HardwareProfile()
308
+
309
+ # CPU Architecture
310
+ profile.cpu_arch = platform.machine().lower()
311
+ profile.cpu_cores = os.cpu_count() or 1
312
+
313
+ # ARM CPU ISA Features
314
+ features = _read_cpuinfo_features()
315
+ profile.cpu_features = features
316
+ profile.has_dotprod = "asimddp" in features
317
+ profile.has_fp16 = "fphp" in features or "asimdhp" in features
318
+ profile.has_i8mm = "i8mm" in features
319
+ profile.has_sve = "sve" in features or "sve2" in features
320
+
321
+ # SoC and GPU identification
322
+ profile.soc_name = _detect_soc_name()
323
+ profile.gpu_name = _detect_gpu_name()
324
+
325
+ # Vulkan & OpenCL driver probe
326
+ vulkan_info = _probe_vulkan_driver()
327
+ if vulkan_info and vulkan_info.usable:
328
+ profile.vulkan_available = True
329
+ profile.vulkan_driver = vulkan_info
330
+
331
+ opencl_info = _probe_opencl_driver()
332
+ if opencl_info and opencl_info.usable:
333
+ profile.opencl_available = True
334
+ profile.opencl_driver = opencl_info
335
+
336
+ # NPU / TPU driver probe
337
+ npu_info = detect_npu_capabilities()
338
+ profile.npu_profile = npu_info
339
+
340
+ # Recommend optimal backend: Vulkan > OpenCL > CPU NEON
341
+ if profile.vulkan_available:
342
+ # Check Qualcomm Adreno 600 series lacking VK_KHR_8bit_storage (e.g. Snapdragon 865 / SM8250 / Adreno 650)
343
+ is_adreno_650 = (
344
+ "650" in str(profile.gpu_name).lower()
345
+ or "sm8250" in str(profile.soc_name).lower()
346
+ or "kona" in str(profile.soc_name).lower()
347
+ or "snapdragon 865" in str(profile.soc_name).lower()
348
+ )
349
+ # Check integer dot product capability on ARM Mali GPUs (e.g. Exynos 1280 / Mali-G68 lacking VK_KHR_shader_integer_dot_product)
350
+ is_mali = (
351
+ "mali" in str(profile.gpu_name).lower()
352
+ or "s5e8825" in str(profile.soc_name).lower()
353
+ or "exynos 1280" in str(profile.soc_name).lower()
354
+ )
355
+ if is_adreno_650:
356
+ if profile.opencl_available:
357
+ profile.recommended_backend = ComputeBackend.OPENCL
358
+ profile.recommended_ngl = 32
359
+ else:
360
+ profile.recommended_backend = ComputeBackend.CPU_NEON
361
+ profile.recommended_ngl = 0
362
+ elif is_mali and not profile.has_dotprod:
363
+ # Under auto mode, Mali without int_dot routes to CPU NEON to avoid descriptor pool exhaustion / shader faults
364
+ profile.recommended_backend = ComputeBackend.CPU_NEON
365
+ profile.recommended_ngl = 0
366
+ else:
367
+ profile.recommended_backend = ComputeBackend.VULKAN
368
+ profile.recommended_ngl = 99
369
+ elif profile.opencl_available:
370
+ profile.recommended_backend = ComputeBackend.OPENCL
371
+ profile.recommended_ngl = 32
372
+ else:
373
+ profile.recommended_backend = ComputeBackend.CPU_NEON
374
+ profile.recommended_ngl = 0
375
+
376
+ # Generate CMake flags
377
+ profile.cmake_extra_flags = _build_cmake_flags(profile)
378
+
379
+ return profile
380
+
381
+
382
+ def _build_cmake_flags(profile: HardwareProfile, backend: Optional[str] = None) -> List[str]:
383
+ """Generate optimal CMake flags for ARM64 Android Bionic."""
384
+ flags = []
385
+ prefix = os.environ.get("PREFIX", "/data/data/com.termux/files/usr")
386
+
387
+ target_backend = backend.lower().strip() if backend else "auto"
388
+
389
+ if target_backend == "cpu":
390
+ flags.append("-DSD_VULKAN=OFF")
391
+ flags.append("-DSD_OPENCL=OFF")
392
+ elif (target_backend in ("auto", "vulkan")) and profile.vulkan_available and profile.vulkan_driver:
393
+ flags.append("-DSD_VULKAN=ON")
394
+ flags.append(f"-DVulkan_LIBRARY={profile.vulkan_driver.library_path}")
395
+ flags.append(f"-DVulkan_INCLUDE_DIR={prefix}/include")
396
+ flags.append("-DGGML_VULKAN_COOPMAT_GLSLC_SUPPORT=OFF")
397
+ flags.append("-DGGML_VULKAN_COOPMAT2_GLSLC_SUPPORT=OFF")
398
+ flags.append("-DGGML_VULKAN_COOPMAT2_DECODE_VECTOR_GLSLC_SUPPORT=OFF")
399
+ flags.append("-DGGML_VULKAN_BFLOAT16_GLSLC_SUPPORT=OFF")
400
+ flags.append("-DCMAKE_EXE_LINKER_FLAGS=-L/system/lib64 -Wl,-rpath,/system/lib64 -L/vendor/lib64 -Wl,-rpath,/vendor/lib64")
401
+ elif (target_backend in ("auto", "opencl")) and profile.opencl_available and profile.opencl_driver:
402
+ flags.append("-DSD_OPENCL=ON")
403
+ flags.append(f"-DOpenCL_LIBRARY={profile.opencl_driver.library_path}")
404
+ flags.append(f"-DOpenCL_INCLUDE_DIR={prefix}/include")
405
+ flags.append("-DCMAKE_EXE_LINKER_FLAGS=-L/vendor/lib64 -Wl,-rpath,/vendor/lib64 -L/system/lib64 -Wl,-rpath,/system/lib64")
406
+ else:
407
+ flags.append("-DSD_VULKAN=OFF")
408
+ flags.append("-DSD_OPENCL=OFF")
409
+
410
+ # CPU ISA optimization flags
411
+ march_parts = ["armv8-a"]
412
+ if profile.cpu_arch in ("aarch64", "arm64"):
413
+ march_parts = ["armv8.2-a"]
414
+ if profile.has_dotprod:
415
+ march_parts.append("dotprod")
416
+ if profile.has_fp16:
417
+ march_parts.append("fp16")
418
+ if profile.has_i8mm:
419
+ march_parts.append("i8mm")
420
+
421
+ if len(march_parts) > 1:
422
+ march_str = "+".join(march_parts)
423
+ flags.append(f"-DCMAKE_C_FLAGS=-O2 -march={march_str} -D_GNU_SOURCE")
424
+ flags.append(f"-DCMAKE_CXX_FLAGS=-O2 -march={march_str} -D_GNU_SOURCE -Wno-deprecated-literal-operator")
425
+ else:
426
+ flags.append("-DCMAKE_C_FLAGS=-O2 -D_GNU_SOURCE")
427
+ flags.append("-DCMAKE_CXX_FLAGS=-O2 -D_GNU_SOURCE -Wno-deprecated-literal-operator")
428
+
429
+ return flags
430
+
431
+
432
+ # ------------------------------------------------------------------------------
433
+ # 3. Device Selection for generate()
434
+ # ------------------------------------------------------------------------------
435
+
436
+ def _resolve_ameva_runtime() -> Optional[Any]:
437
+ try:
438
+ import importlib.util
439
+ spec = importlib.util.find_spec("ameva_runtime")
440
+ if spec is not None:
441
+ import ameva_runtime
442
+ return ameva_runtime
443
+ except (ImportError, AttributeError):
444
+ pass
445
+ return None
446
+
447
+
448
+ GPU_RUNTIME_MIN_VERSION = "2.0.0"
449
+
450
+
451
+ def _require_gpu_runtime(requested_device: str) -> None:
452
+ """Stop with the shared AMU_ERR_RUNTIME_REQUIRED explanation unless ameva-runtime is usable.
453
+
454
+ Every GPU request (vulkan, gpu, opencl) goes through here. There is no CPU fallback: the
455
+ caller gets RuntimeRequiredError and the CLI exits with status 69.
456
+ """
457
+ try:
458
+ from ameva_device import AmevaRuntimeRequiredError, require_ameva_runtime
459
+ except ImportError as exc:
460
+ raise RuntimeRequiredError(
461
+ "[AMU_ERR_RUNTIME_REQUIRED] termux-diffusion: GPU execution (device='%s') needs ameva-runtime (AMUDA), "
462
+ "and the shared check library 'ameva-device-sdk' is missing from this installation.\n"
463
+ "Nothing was run, and no CPU fallback was used.\n"
464
+ "Install:\n"
465
+ " pip install \"ameva-runtime>=%s\"\n"
466
+ " npm install -g @ameva/runtime\n"
467
+ " Releases and documentation: https://github.com/uno-km/ameva-runtime-releases\n"
468
+ "To run on the CPU instead, use --device cpu.\n" % (requested_device, GPU_RUNTIME_MIN_VERSION)
469
+ ) from exc
470
+ try:
471
+ require_ameva_runtime(
472
+ "termux-diffusion", "GPU execution (device='%s')" % requested_device,
473
+ min_version=GPU_RUNTIME_MIN_VERSION,
474
+ )
475
+ except AmevaRuntimeRequiredError as exc:
476
+ raise RuntimeRequiredError(str(exc)) from exc
477
+
478
+
479
+ def resolve_device_backend(requested_device: str) -> Tuple[str, int]:
480
+ """Resolve the user's device= argument to an actual backend and ngl count.
481
+
482
+ Zero-Silent-Fallback & Strict Option Rules:
483
+ - 'cpu': User explicitly requested CPU. 100% pure ARM NEON execution without touching GPU.
484
+ - 'vulkan' / 'gpu': Explicit GPU mode. Halts immediately with Fail-Fast if GPU driver missing.
485
+ - 'opencl': Explicit OpenCL mode. Halts immediately if OpenCL missing.
486
+ - 'auto': Evaluates hardware capabilities (int_dot on Mali, Vulkan driver).
487
+ """
488
+ req = requested_device.lower().strip()
489
+
490
+ # Rule 1: Strict CPU Priority - If user explicitly passed CPU, NEVER touch GPU or fail-fast probes
491
+ if req == "cpu":
492
+ return "cpu", 0
493
+
494
+ # Rule 2: every GPU request (vulkan, gpu, opencl) needs ameva-runtime (AMUDA). An explicit
495
+ # request stops here with the shared install guidance; there is no CPU fallback.
496
+ if req in ("vulkan", "gpu", "opencl", "cl"):
497
+ _require_gpu_runtime(req)
498
+
499
+ ameva_mod = _resolve_ameva_runtime()
500
+
501
+ if req == "auto":
502
+ if ameva_mod is None:
503
+ sys.stderr.write(
504
+ "[termux-diffusion] GPU not used: ameva-runtime (AMUDA) is not installed, so this run uses the CPU. "
505
+ "To enable the GPU: pip install ameva-runtime (https://github.com/uno-km/ameva-runtime-releases)\n"
506
+ )
507
+ sys.stderr.flush()
508
+ return "cpu", 0
509
+
510
+ profile = detect_hardware_profile()
511
+
512
+ if req == "auto":
513
+ backend = profile.recommended_backend
514
+ ngl = profile.recommended_ngl
515
+ logger.info(
516
+ "Auto-detected optimal backend: %s (SoC: %s, GPU: %s)",
517
+ backend.value, profile.soc_name, profile.gpu_name
518
+ )
519
+ return backend.value, ngl
520
+
521
+ if req in ("npu", "tpu"):
522
+ npu_desc = (
523
+ f"{profile.npu_profile.chipset_name} / {profile.npu_profile.dsp_architecture}"
524
+ if (profile.npu_profile and profile.npu_profile.available)
525
+ else "Hardware not detected"
526
+ )
527
+ raise PlatformNotSupportedError(
528
+ f"Native NPU/TPU acceleration ({npu_desc}) requires Qualcomm QNN / LiteRT C++ Graph Runtime (scheduled for v2.0 roadmap). "
529
+ f"Currently, full hardware acceleration is supported via Vulkan GPU (device='vulkan') and ARM NEON (device='cpu'). "
530
+ f"Please use device='vulkan' or device='auto'."
531
+ )
532
+
533
+ if req in ("vulkan", "gpu"):
534
+ if profile.vulkan_available:
535
+ return "vulkan", 99
536
+ raise PlatformNotSupportedError(
537
+ "[ERROR: AMEVA-DIFFUSION-E003] Vulkan GPU acceleration was explicitly requested (device='vulkan' / 'gpu'), "
538
+ "but no accessible Vulkan driver (.so) was found on this system. "
539
+ "Execution halted strictly without silent fallback to prevent unexpected CPU execution."
540
+ )
541
+
542
+ if req == "opencl":
543
+ if profile.opencl_available:
544
+ return "opencl", 32
545
+ raise PlatformNotSupportedError(
546
+ "OpenCL acceleration was explicitly requested (device='opencl'), "
547
+ "but no accessible OpenCL driver (.so) was found on this system. "
548
+ "Execution halted strictly without silent fallback to prevent unexpected CPU execution."
549
+ )
550
+
551
+ raise ValueError(f"Unknown computing device '{requested_device}'. Supported: 'auto', 'cpu', 'vulkan', 'opencl'.")
552
+
553
+
554
+ def get_sd_cli_gpu_args(device: str, ngl: int) -> List[str]:
555
+ """Build the sd-cli command-line arguments for GPU/NPU offloading."""
556
+ args: List[str] = []
557
+ if device == "cpu":
558
+ args.append("--offload-to-cpu")
559
+ return args
560
+
561
+
562
+ def format_hardware_report(profile: HardwareProfile) -> str:
563
+ """Format a human-readable hardware capability report for doctor/diagnostics."""
564
+ lines = [
565
+ "=== Hardware Acceleration Profile ===",
566
+ f"SoC: {profile.soc_name}",
567
+ f"GPU Architecture: {profile.gpu_name}",
568
+ f"CPU Architecture: {profile.cpu_arch} ({profile.cpu_cores} cores)",
569
+ f"ARM SIMD Extensions: DotProd={'[OK]' if profile.has_dotprod else '[FAIL]'} "
570
+ f"FP16={'[OK]' if profile.has_fp16 else '[FAIL]'} "
571
+ f"I8MM={'[OK]' if profile.has_i8mm else '[FAIL]'} "
572
+ f"SVE={'[OK]' if profile.has_sve else '[FAIL]'}",
573
+ f"GPU Vulkan: {'Available [OK]' if profile.vulkan_available else 'Not Found [WARN]'}",
574
+ ]
575
+ if profile.vulkan_driver:
576
+ lines.append(f" -> Vulkan Lib: {profile.vulkan_driver.library_path}")
577
+ lines.append(f"GPU OpenCL: {'Available [OK]' if profile.opencl_available else 'Not Found [WARN]'}")
578
+ if profile.opencl_driver:
579
+ lines.append(f" -> OpenCL Lib: {profile.opencl_driver.library_path}")
580
+
581
+ if profile.npu_profile and profile.npu_profile.available:
582
+ lines.append(f"NPU / TPU Hardware: Detected [INFO] ({profile.npu_profile.dsp_architecture})")
583
+ lines.append(f" -> Peak Hardware Spec: {profile.npu_profile.tops_rating} TOPS")
584
+ lines.append(f" -> Driver Library: {profile.npu_profile.driver_library}")
585
+ lines.append(f" -> Runtime Status: Native QNN C++ execution scheduled for v2.0 (Active: Vulkan GPU & ARM NEON)")
586
+ else:
587
+ lines.append("NPU / TPU Hardware: Not Detected (CPU/GPU pipeline active)")
588
+
589
+ lines.append(f"Recommended Compute Backend: {profile.recommended_backend.value}")
590
+ lines.append(f"Recommended GPU/NPU Offload Layers: {profile.recommended_ngl}")
591
+ if profile.cmake_extra_flags:
592
+ lines.append(f"CMake Build Flags: {' '.join(profile.cmake_extra_flags)}")
593
+ lines.append("=" * 40)
594
+ return "\n".join(lines)
595
+
596
+
597
+ # ------------------------------------------------------------------------------
598
+ # 4. Profile Gating Manager (Runtime Model/Preset Safety)
599
+ # ------------------------------------------------------------------------------
600
+
601
+ import json
602
+
603
+ @dataclass
604
+ class ProfileGatingManager:
605
+ """Manages runtime matching against validated-vulkan-profiles.json for safe device/model execution."""
606
+ profiles: List[Dict] = field(default_factory=list)
607
+
608
+ @classmethod
609
+ def load_from_json(cls, json_path: Optional[Path] = None) -> "ProfileGatingManager":
610
+ if json_path is None:
611
+ json_path = Path(__file__).parent / "data" / "validated-vulkan-profiles.json"
612
+ if json_path.is_file():
613
+ try:
614
+ data = json.loads(json_path.read_text(encoding="utf-8"))
615
+ return cls(profiles=data.get("profiles", []))
616
+ except Exception as e:
617
+ logger.warning("Failed to load validated-vulkan-profiles.json: %s", e)
618
+ return cls(profiles=[])
619
+
620
+ def find_matching_profile(self, model_name: str, soc_name: str = "", gpu_name: str = "") -> Optional[Dict]:
621
+ model_clean = model_name.strip().upper()
622
+ soc_clean = soc_name.strip().upper()
623
+ gpu_clean = gpu_name.strip().upper()
624
+
625
+ for prof in self.profiles:
626
+ target_model = prof.get("device_model", "").strip().upper()
627
+ if target_model and (target_model == model_clean or target_model in model_clean or model_clean in target_model):
628
+ return prof
629
+ aliases = [a.strip().upper() for a in prof.get("device_aliases", [])]
630
+ if any(a == model_clean or a in model_clean for a in aliases):
631
+ return prof
632
+
633
+ # Fallback matching by SoC / GPU
634
+ for prof in self.profiles:
635
+ prof_soc = prof.get("soc", "").strip().upper()
636
+ prof_gpu = prof.get("gpu", "").strip().upper()
637
+ if prof_soc and soc_clean and (prof_soc in soc_clean or soc_clean in prof_soc):
638
+ return prof
639
+ if prof_gpu and gpu_clean and (prof_gpu in gpu_clean or gpu_clean in prof_gpu):
640
+ return prof
641
+ return None
642
+
643
+ def validate_execution(self, preset_or_model: str, device: str, user_explicit: bool = False) -> Tuple[bool, Optional[str]]:
644
+ """Validate if the requested model/preset and device backend are safe for current hardware.
645
+
646
+ Strict User Policy:
647
+ If user explicitly requested GPU execution (user_explicit=True), bypass safety gating
648
+ and let the native engine execute directly, failing fast if the hardware driver crashes.
649
+ """
650
+ if user_explicit:
651
+ logger.info("Explicit GPU device requested by user. Bypassing safety gating check for '%s'.", preset_or_model)
652
+ return True, None
653
+
654
+ device_clean = device.lower().strip()
655
+ if device_clean not in ("vulkan", "gpu"):
656
+ return True, None
657
+
658
+ dev_model = _detect_device_model()
659
+ soc_name = _detect_soc_name()
660
+ gpu_name = _detect_gpu_name()
661
+ matched = self.find_matching_profile(dev_model, soc_name, gpu_name)
662
+ if not matched:
663
+ return True, None
664
+
665
+ # 1. Check blocked list
666
+ blocked = matched.get("blocked", [])
667
+ preset_clean = preset_or_model.lower().strip()
668
+ for blk in blocked:
669
+ fam = blk.get("model_family", "").lower()
670
+ backend = blk.get("backend", "").lower()
671
+ if backend in (device_clean, "all") and fam in preset_clean:
672
+ reason = blk.get("reason", "Incompatible hardware backend configuration")
673
+ return False, (
674
+ f"Model '{preset_or_model}' is blocked on device '{matched.get('device_model')}' "
675
+ f"under backend '{device_clean}' due to: {reason}."
676
+ )
677
+
678
+ # 2. Check preset gating if preset exists in profile
679
+ presets = matched.get("presets", {})
680
+ if preset_clean in presets:
681
+ p_info = presets[preset_clean]
682
+ if p_info.get("status") == "pending_device_validation" and not p_info.get("auto_activation", True):
683
+ return False, (
684
+ f"Preset '{preset_clean}' is currently pending device validation on '{matched.get('device_model')}' "
685
+ f"and auto_activation is disabled."
686
+ )
687
+
688
+ return True, None
689
+
690
+
691
+ _gating_manager: Optional[ProfileGatingManager] = None
692
+
693
+
694
+ def get_profile_gating_manager() -> ProfileGatingManager:
695
+ """Get singleton instance of ProfileGatingManager."""
696
+ global _gating_manager
697
+ if _gating_manager is None:
698
+ _gating_manager = ProfileGatingManager.load_from_json()
699
+ return _gating_manager
700
+
701
+
702
+ def resolve_hardware(model_name: str = "sdxs-512-dreamshaper", backend: str = "vulkan") -> Dict[str, Any]:
703
+ """Resolves execution plan via ameva-runtime HAL SSOT."""
704
+ try:
705
+ from ameva_runtime.adapters import DiffusionAdapter
706
+ except ImportError as err:
707
+ raise PlatformNotSupportedError(
708
+ "[ERROR: AMEVA-DIFFUSION-E001] ameva-runtime HAL is not installed.\n"
709
+ "Run: pip install ameva-runtime\n"
710
+ "Raw Cause: " + str(err)
711
+ ) from err
712
+
713
+ adapter = DiffusionAdapter()
714
+ plan = adapter.bind(model_name=model_name, requested_backend=backend)
715
+ if backend == "vulkan" and plan.backend != "vulkan":
716
+ raise PlatformNotSupportedError(
717
+ f"[ERROR: AMEVA-DIFFUSION-E003] Vulkan requested but unsupported on this hardware.\n"
718
+ f"Diagnosis: {plan.diagnosis_reason}"
719
+ )
720
+ return plan.to_dict()
721
+
722
+
723
+
724
+
725
+ # Standard Unified Interface Bridges
726
+ def is_termux() -> bool:
727
+ try:
728
+ from .platform import is_android_termux
729
+ return is_android_termux()
730
+ except Exception:
731
+ import os
732
+ return bool(os.environ.get("TERMUX_VERSION") or os.environ.get("TERMUX_APP_PID"))
733
+
734
+
735
+ def is_android() -> bool:
736
+ return is_termux()
737
+
738
+
739
+ def detect_hardware() -> HardwareProfile:
740
+ """Standard Unified Hardware Detection for termux-diffusion."""
741
+ return detect_hardware_profile()
742
+
743
+
744
+ resolve_device = resolve_device_backend
745
+
746
+
747
+ def get_optimal_threads() -> int:
748
+ """Standard Unified Optimal Threads Calculator for termux-diffusion."""
749
+ return detect_hardware().recommended_threads
750
+
751
+
752
+ def bind_hardware(engine: Any, requested_device: str = "auto", **kwargs) -> Optional[Any]:
753
+ """Standard Unified Hardware Binding Interface for termux-diffusion."""
754
+ device, ngl = resolve_device_backend(requested_device)
755
+ if hasattr(engine, "device"):
756
+ setattr(engine, "device", device)
757
+ return engine
758
+
759
+
760
+ def get_kernel_physical_memory_gb() -> Tuple[float, float]:
761
+ """Retrieve deterministic physical memory metrics (MemTotal, MemAvailable) in GiB.
762
+
763
+ Directly parses Linux kernel /proc/meminfo to avoid string heuristics.
764
+ Falls back to psutil or conservative defaults if /proc/meminfo is inaccessible.
765
+ """
766
+ mem_total_bytes = 0
767
+ mem_avail_bytes = 0
768
+ meminfo_path = Path("/proc/meminfo")
769
+
770
+ if meminfo_path.exists():
771
+ try:
772
+ with open(meminfo_path, "r", encoding="utf-8") as f:
773
+ for line in f:
774
+ if line.startswith("MemTotal:"):
775
+ parts = line.split()
776
+ if len(parts) >= 2:
777
+ mem_total_bytes = int(parts[1]) * 1024
778
+ elif line.startswith("MemAvailable:"):
779
+ parts = line.split()
780
+ if len(parts) >= 2:
781
+ mem_avail_bytes = int(parts[1]) * 1024
782
+ if mem_total_bytes > 0 and mem_avail_bytes > 0:
783
+ break
784
+ except Exception as e:
785
+ logger.warning("Failed to parse /proc/meminfo: %s", e)
786
+
787
+ if mem_total_bytes == 0:
788
+ try:
789
+ import psutil
790
+ vm = psutil.virtual_memory()
791
+ mem_total_bytes = vm.total
792
+ mem_avail_bytes = vm.available
793
+ except Exception:
794
+ # Conservative fallback
795
+ mem_total_bytes = 4 * (1024 ** 3)
796
+ mem_avail_bytes = 2 * (1024 ** 3)
797
+
798
+ return round(mem_total_bytes / (1024 ** 3), 2), round(mem_avail_bytes / (1024 ** 3), 2)
799
+
800
+
801
+ _LAST_CPU_SAMPLE: Optional[Tuple[float, float, float]] = None
802
+
803
+
804
+ def get_live_cpu_percent() -> float:
805
+ """Retrieve instantaneous CPU utilization percentage (0-100%) from Linux /proc/stat."""
806
+ global _LAST_CPU_SAMPLE
807
+ now = time.time()
808
+ stat_path = Path("/proc/stat")
809
+
810
+ try:
811
+ if stat_path.exists():
812
+ with open(stat_path, "r", encoding="utf-8") as f:
813
+ first_line = f.readline()
814
+ if first_line.startswith("cpu "):
815
+ parts = [float(x) for x in first_line.split()[1:8]]
816
+ idle = parts[3] + (parts[4] if len(parts) > 4 else 0.0)
817
+ total = sum(parts)
818
+ work = total - idle
819
+
820
+ if _LAST_CPU_SAMPLE is not None:
821
+ last_work, last_total, last_time = _LAST_CPU_SAMPLE
822
+ delta_total = total - last_total
823
+ delta_work = work - last_work
824
+ _LAST_CPU_SAMPLE = (work, total, now)
825
+ if delta_total > 0 and (now - last_time) >= 0.1:
826
+ pct = (delta_work / delta_total) * 100.0
827
+ return round(min(100.0, max(0.0, pct)), 1)
828
+ _LAST_CPU_SAMPLE = (work, total, now)
829
+ except (OSError, PermissionError):
830
+ pass
831
+
832
+ try:
833
+ load1, _, _ = os.getloadavg()
834
+ cpu_cnt = os.cpu_count() or 8
835
+ pct = (load1 / cpu_cnt) * 100.0
836
+ return round(min(100.0, max(0.0, pct)), 1)
837
+ except (OSError, AttributeError):
838
+ pass
839
+
840
+ try:
841
+ import psutil
842
+ return round(float(psutil.cpu_percent(interval=None)), 1)
843
+ except Exception:
844
+ return 0.0
845
+
846
+
847
+ def get_live_gpu_percent() -> Optional[int]:
848
+ """Query Linux/Android kernel sysfs GPU load percentage across Adreno and Mali chips."""
849
+ kgsl_paths = [
850
+ Path("/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage"),
851
+ Path("/sys/class/kgsl/kgsl-3d0/gpubusy"),
852
+ ]
853
+ for p in kgsl_paths:
854
+ try:
855
+ if p.is_file():
856
+ content = p.read_text().strip()
857
+ if " " in content:
858
+ parts = content.split()
859
+ busy_val, total_val = int(parts[0]), int(parts[1])
860
+ if total_val > 0:
861
+ return int(round((busy_val / total_val) * 100))
862
+ else:
863
+ return int(content.replace("%", ""))
864
+ except (OSError, PermissionError):
865
+ pass
866
+
867
+ mali_paths = [
868
+ Path("/sys/class/misc/mali0/device/utilization"),
869
+ Path("/sys/devices/platform/17000000.gpu/utilization"),
870
+ Path("/sys/kernel/gpu/gpu_busy"),
871
+ ]
872
+ for p in mali_paths:
873
+ try:
874
+ if p.is_file():
875
+ content = p.read_text().strip().replace("%", "")
876
+ return int(content)
877
+ except (OSError, PermissionError):
878
+ pass
879
+
880
+ return None
881
+
882
+
883
+ def get_system_resource_metrics() -> Dict[str, Any]:
884
+ """Deterministic system health probe returning RAM, CPU, GPU, and process memory."""
885
+ mem_total_gb, mem_avail_gb = get_kernel_physical_memory_gb()
886
+ mem_used_gb = round(max(0.0, mem_total_gb - mem_avail_gb), 2)
887
+ mem_pct = round((mem_used_gb / mem_total_gb) * 100.0, 1) if mem_total_gb > 0 else 0.0
888
+
889
+ cpu_pct = get_live_cpu_percent()
890
+ gpu_busy = get_live_gpu_percent()
891
+
892
+ rss_mb = 0.0
893
+ try:
894
+ with open(f"/proc/{os.getpid()}/status", "r", encoding="utf-8") as f:
895
+ for line in f:
896
+ if line.startswith("VmRSS:"):
897
+ parts = line.split()
898
+ if len(parts) >= 2:
899
+ rss_mb = round(int(parts[1]) / 1024.0, 1)
900
+ break
901
+ except Exception:
902
+ pass
903
+
904
+ return {
905
+ "ram_total_gb": mem_total_gb,
906
+ "ram_avail_gb": mem_avail_gb,
907
+ "ram_used_gb": mem_used_gb,
908
+ "ram_percent": mem_pct,
909
+ "cpu_percent": cpu_pct,
910
+ "gpu_percent": gpu_busy,
911
+ "process_rss_mb": rss_mb,
912
+ }
913
+
914
+
915
+ class TermuxExecutionGuard:
916
+ """Execution context guard that manages Android partial wake locks and scheduling niceness.
917
+
918
+ Prevents CPU sleep / Doze throttling during model inference and restores state cleanly.
919
+ """
920
+ def __init__(self, enable_wake_lock: bool = True, boost_priority: bool = True, safe_mode: bool = False):
921
+ self.is_termux_env = is_termux() or os.path.exists("/data/data/com.termux")
922
+ self.enable_wake_lock = enable_wake_lock
923
+ self.boost_priority = boost_priority and not safe_mode
924
+ self.safe_mode = safe_mode
925
+ self.wake_lock_acquired = False
926
+
927
+ def __enter__(self):
928
+ if self.is_termux_env:
929
+ if self.safe_mode:
930
+ try:
931
+ os.nice(10)
932
+ logger.info("[TermuxGuard] Safe Mode active: Process niceness demoted (+10) to preserve SurfaceFlinger and background apps")
933
+ except Exception:
934
+ pass
935
+ elif self.boost_priority:
936
+ try:
937
+ os.nice(-20)
938
+ logger.info("[TermuxGuard] Process niceness boosted to highest priority (-20)")
939
+ except Exception:
940
+ try:
941
+ os.nice(-10)
942
+ except Exception:
943
+ pass
944
+
945
+ if self.enable_wake_lock:
946
+ wake_bin = shutil.which("termux-wake-lock") or "/data/data/com.termux/files/usr/bin/termux-wake-lock"
947
+ if os.path.exists(wake_bin):
948
+ try:
949
+ subprocess.run([wake_bin], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
950
+ self.wake_lock_acquired = True
951
+ logger.info("[TermuxGuard] Android PARTIAL_WAKE_LOCK acquired via termux-wake-lock")
952
+ except Exception as e:
953
+ logger.warning("[TermuxGuard] Could not invoke termux-wake-lock: %s", e)
954
+ else:
955
+ try:
956
+ subprocess.run(
957
+ ["am", "startservice", "--user", "0", "-a", "com.termux.service_wake_lock", "com.termux/.app.TermuxService"],
958
+ check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
959
+ )
960
+ self.wake_lock_acquired = True
961
+ logger.info("[TermuxGuard] Android PARTIAL_WAKE_LOCK acquired via TermuxService Intent")
962
+ except Exception as e:
963
+ logger.warning("[TermuxGuard] Could not start TermuxService wake_lock: %s", e)
964
+ return self
965
+
966
+ def __exit__(self, exc_type, exc_val, exc_tb):
967
+ if self.is_termux_env and self.wake_lock_acquired:
968
+ wake_unlock = shutil.which("termux-wake-unlock") or "/data/data/com.termux/files/usr/bin/termux-wake-unlock"
969
+ if os.path.exists(wake_unlock):
970
+ try:
971
+ subprocess.run([wake_unlock], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
972
+ logger.info("[TermuxGuard] Android PARTIAL_WAKE_LOCK released cleanly")
973
+ except Exception:
974
+ pass
975
+ else:
976
+ try:
977
+ subprocess.run(
978
+ ["am", "startservice", "--user", "0", "-a", "com.termux.service_wake_unlock", "com.termux/.app.TermuxService"],
979
+ check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
980
+ )
981
+ logger.info("[TermuxGuard] Android PARTIAL_WAKE_LOCK released via TermuxService Intent")
982
+ except Exception:
983
+ pass
984
+
985
+