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,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
|
+
|