diff --git a/H3_NATIVE_TEMPORAL.md b/H3_NATIVE_TEMPORAL.md index 8e93e71..62ee398 100644 --- a/H3_NATIVE_TEMPORAL.md +++ b/H3_NATIVE_TEMPORAL.md @@ -94,9 +94,9 @@ The console logs the first attention's input shape and completion time and the f ## One-pass native Kitchen kernels (1.115.16) -**From 1.115.17, BFS prepares the one-pass extension automatically on first use of Kitchen/sol-attn on supported Linux x86_64 SM120 systems with a driver supporting CUDA 13.** It downloads a fixed release into ComfyUI’s user cache, verifies embedded SHA256 hashes and ABI/architecture, and activates it in that execution. No pip install, package replacement or sampling-time compilation is required. Other systems retain the compatibility backends or can use an explicitly built manual overlay. +**From 1.115.22, BFS selects a pinned one-pass extension automatically for Linux x86_64 SM80/86/89/90/100/120 and Windows x86_64 SM86/89/120, with a driver supporting CUDA 13.** It downloads a fixed release into ComfyUI’s user cache, verifies embedded SHA256 hashes and ABI/architecture, and activates it in that execution. No pip install, package replacement or sampling-time compilation is required. Other systems retain the compatibility backends or can use an explicitly built manual overlay. -For an explicit installation or source build, use the [Kitchen fork's setup guide](https://github.com/alisson-anjos/comfy-kitchen/blob/feat/h3-native-temporal/H3_NATIVE_TEMPORAL_KERNELS.md). An [experimental Linux x86_64 SM120 prebuilt](https://github.com/alisson-anjos/comfy-kitchen/releases/tag/h3-native-temporal-kernel-v1) is available, including SHA256SUMS, so sampling need not compile anything. RTX 5090 shares SM120 with the tested RTX PRO 6000, but full video behavior/performance on the 5090 still requires validation. The installer adds a separate temporal module/library while preserving ordinary Kitchen's `_C` and operators. Source builds use `nvcc` explicitly; no per-shape torch.compile is used. +For an explicit installation or source build, use the [Kitchen fork's setup guide](https://github.com/alisson-anjos/comfy-kitchen/blob/feat/h3-native-temporal/H3_NATIVE_TEMPORAL_KERNELS.md). The [experimental nine-binary release](https://github.com/alisson-anjos/comfy-kitchen/releases/tag/temporal-native-0413d43878606425a84aa400f1e5da50ed9dcdad) is available, including SHA256SUMS, so sampling need not compile anything. RTX 5090 shares SM120 with the tested RTX PRO 6000, but full video behavior/performance on the 5090 still requires validation. The installer adds a separate temporal module/library while preserving ordinary Kitchen's `_C` and operators. Source builds use `nvcc` explicitly; no per-shape torch.compile is used. Keep the existing MODEL wiring and native temporal guide mode. Model Attention Backend's Kitchen choice selects native dense when available. Model Sparse Attention's sol-attn choice selects native Sol during its eligible sparse window; its dense fallback retains the temporal policy too. No new UI switch is required. Console confirmation: @@ -125,24 +125,39 @@ The sections below retain the earlier compatibility implementations and measurem ## Automatic first-use setup (1.115.17) -Choose Kitchen or sol-attn normally and run the native temporal node. On supported Linux x86_64 / NVIDIA SM120 / CUDA-13-driver systems, it prepares the extension during node execution when the model's load device is known, before sampling. A lazy fallback can prepare it on the first attention call too. The ordinary Flash/Flex path does not request an installation. +Choose Kitchen or sol-attn normally and run the native temporal node. On supported Linux/Windows x86_64 / NVIDIA architectures in the matrix below / CUDA-13-driver systems, it prepares the extension during node execution when the model's load device is known, before sampling. A lazy fallback can prepare it on the first attention call too. The ordinary Flash/Flex path does not request an installation. -The default cache is `ComfyUI user directory/bfs/h3_temporal_kernels/`; standalone use falls back to `~/.cache/bfsnodes/h3_temporal_kernels/`. Only the pinned Python API (~10 KB), SM120 library (~3.3 MB), LICENSE and NOTICE are downloaded. The implementation writes to its own cache, not `site-packages`, and leaves ordinary Kitchen's `_C` untouched. Existing compatible manual overlays are preferred. +The default cache is `ComfyUI user directory/bfs/h3_temporal_kernels/`; standalone use falls back to `~/.cache/bfsnodes/h3_temporal_kernels/`. Only the pinned Python API (~10 KB), the matching platform/GPU library (~2.1–3.4 MB), LICENSE and NOTICE are downloaded. The OS, architecture, release hash and ABI select the cache; Linux libraries and Windows DLLs are never substituted for one another. The implementation writes to its own cache, not `site-packages`, and leaves ordinary Kitchen's `_C` untouched. Existing compatible manual overlays are preferred. Security/correctness properties: immutable source commit and embedded hashes; bounded download sizes; temporary downloads replaced atomically only after SHA256 validation; native ABI and GPU architecture checks before Python entry-point execution; thread/process installation locks; one setup attempt per device/cache location per process. An incomplete, incompatible or offline first installation logs its reason and retains the previous backend. A verified cache works in a new process without network access. No model, prompt or reference data is uploaded. Expected console messages: ```text -First-use setup: downloading the verified one-pass extension (~3.3 MB). No compilation or pip install. -Verified one-pass extension ready: ... +First-use setup: downloading verified Linux SM89 one-pass extension (3.2 MB). No compilation or pip install. +Verified Linux SM89 one-pass extension ready: ... Kitchen ONE-PASS dense kernel ACTIVE # or: Kitchen ONE-PASS sol kernel ACTIVE ``` The addon activates without a second restart during that first workflow. Restarting ComfyUI is still required after updating BFS's own Python code. `BFS_H3_TEMPORAL_AUTO_INSTALL=0` disables automatic setup; `BFS_H3_TEMPORAL_CACHE_DIR=/path/to/cache` chooses another cache location. For a failed setup, restart to retry or use the manual installer linked above. Unsupported systems do not trigger an automatic source build. -A real first-use download, ABI check, and dense/Sol GPU calls passed. A separate fresh process with network requests deliberately disabled reused the verified cache successfully. [Recorded first-use/offline checks](docs/benchmarks/h3_temporal_auto_setup.json). +The original SM120-only release passed a real first-use download, ABI check, and dense/Sol GPU calls. This does not GPU-validate the newer multi-architecture binaries. A separate fresh process with network requests deliberately disabled reused the verified cache successfully. [Recorded first-use/offline checks](docs/benchmarks/h3_temporal_auto_setup.json). + +### Published architecture matrix (1.115.22) + +| OS, 64-bit x86 | Published GPU architectures | CI checks | GPU execution of this release | +| --- | --- | --- | --- | +| Linux | SM80, SM86, SM89, SM90, SM100, SM120 | CUDA13 compilation, SHA256, native ABI/arch exports | Not established across the matrix | +| Windows | SM86, SM89, SM120 | CUDA13 compilation, SHA256, native ABI/arch exports on Windows runners | Not established | + +[Download binaries](https://github.com/alisson-anjos/comfy-kitchen/releases/tag/temporal-native-0413d43878606425a84aa400f1e5da50ed9dcdad). The release contains BUILD_MANIFEST.json and SHA256SUMS. BFS uses the reviewed local [pinned manifest](h3_temporal_binaries.json), not an unverified remote latest-release lookup. The [CI workflow](https://github.com/alisson-anjos/comfy-kitchen/blob/0413d43878606425a84aa400f1e5da50ed9dcdad/.github/workflows/temporal-windows.yml) rebuilds/publishes per-commit experimental assets on relevant source changes; adopting a new release requires updating BFS's pin. + +All nine asset sizes/hashes were independently checked; all six Linux ABI/arch exports were loaded without kernel execution. A real SM89 Linux download, checksum, ABI check and Python-API import also passed with CUDA disabled: [record](docs/benchmarks/h3_temporal_matrix_setup.json). CPU fixtures cover Windows DLL selection, driver lookup and locking; these do not replace a real Windows/GPU workflow test. Old SM120 numerical/timing results are not validation of every new binary. + +The NVIDIA driver must support CUDA13 even if PyTorch uses CUDA12.x. Linux needs a compatible glibc/libstdc++ runtime; Windows may need the Microsoft Visual C++ runtime. A missing driver/runtime, incompatible ABI, unsupported GPU/OS or download failure logs the reason and keeps the compatibility backend; no pip install, system-library replacement or compiler launch is attempted. Frame-wise Kitchen fallback can be slower; Flash Partitions remains a separate explicit selection. ARM, macOS and AMD are not covered by these CUDA prebuilts. + +Examples: SM89 covers RTX40 and RTX6000Ada; SM120 covers RTX50 and RTXPRO6000Blackwell; A6000 is SM86. A card name containing “6000” does not identify one architecture. ## Comfy Kitchen and Model Attention Backend (1.115.13) diff --git a/README.md b/README.md index 7ba1394..c214b87 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Experimental native temporal motion-transfer inference (standalone node and Shot [Illustrated explanation: native temporal alignment with compile-free Flash Partitions](docs/H3_FLASH_PARTITIONS.md) covers the exact temporal rule, positive partition decomposition, log-sum-exp merging, measured performance, guide scaling and current backend limitations. -Experimental temporal mode automatically recognizes **Model Attention Backend** (Kitchen/PyTorch) and, from 1.115.14, **Model Sparse Attention → sol-attn**. Version 1.115.17 automatically prepares the verified extension on first use on supported Linux SM120 systems. Version 1.115.16+ can use the [one-pass Kitchen extension](H3_NATIVE_TEMPORAL.md#one-pass-native-kitchen-kernels-111516) for dense INT8 and sol-attn. The earlier compatibility path repeats frame-wise calls and was slower than Flash Partitions; see [setup, measurements and limitations](H3_NATIVE_TEMPORAL.md#model-sparse-attention-sol-attn-compatibility-111514). SLA/VSA remain unsupported. +Experimental temporal mode automatically recognizes **Model Attention Backend** (Kitchen/PyTorch) and, from 1.115.14, **Model Sparse Attention → sol-attn**. Version 1.115.22 automatically selects verified prebuilts for Linux SM80/86/89/90/100/120 and Windows SM86/89/120 (x86_64, CUDA13-capable driver); cross-platform GPU validation remains experimental. Version 1.115.16+ can use the [one-pass Kitchen extension](H3_NATIVE_TEMPORAL.md#one-pass-native-kitchen-kernels-111516) for dense INT8 and sol-attn. The earlier compatibility path repeats frame-wise calls and was slower than Flash Partitions; see [setup, measurements and limitations](H3_NATIVE_TEMPORAL.md#model-sparse-attention-sol-attn-compatibility-111514). SLA/VSA remain unsupported. ![H3 temporal weight decomposition](docs/illustrations/h3_temporal_weight_decomposition.png) diff --git a/docs/benchmarks/h3_temporal_matrix_setup.json b/docs/benchmarks/h3_temporal_matrix_setup.json new file mode 100644 index 0000000..f7251ce --- /dev/null +++ b/docs/benchmarks/h3_temporal_matrix_setup.json @@ -0,0 +1,15 @@ +{ + "platform": "Linux", + "arch": 89, + "checks": [ + "release download", + "SHA256", + "native ABI1", + "native arch89", + "Python API import" + ], + "gpu_execution": false, + "api_version": "h3_native_temporal_kernel_v1", + "release": "temporal-native-0413d43878606425a84aa400f1e5da50ed9dcdad", + "cuda_initialized": false +} diff --git a/h3_temporal_binaries.json b/h3_temporal_binaries.json new file mode 100644 index 0000000..59b6622 --- /dev/null +++ b/h3_temporal_binaries.json @@ -0,0 +1,106 @@ +{ + "release_tag": "temporal-native-0413d43878606425a84aa400f1e5da50ed9dcdad", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "minimum_cuda_driver_version": 13000, + "binaries": [ + { + "native_abi": 1, + "arch": 80, + "sha256": "87f3c33fec7480b480335ed09257488b3eb3052eca6dd3ab9a11477ce7213784", + "bytes": 3307288, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm80.so" + }, + { + "native_abi": 1, + "arch": 86, + "sha256": "b42c1363ad1c360416b57ffe6e215df2202c1510792bea9f6bef34b463aec955", + "bytes": 3303192, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm86.so" + }, + { + "native_abi": 1, + "arch": 89, + "sha256": "0cafbbb543f35cf7cba1be1810f01203cd93adbeac3f64297540d9a822bfbb75", + "bytes": 3155736, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm89.so" + }, + { + "native_abi": 1, + "arch": 90, + "sha256": "825f6aedab25933a9f8e904ae40db5fd8d11d924c08edfa8fa7c070059b928de", + "bytes": 3204888, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm90.so" + }, + { + "native_abi": 1, + "arch": 100, + "sha256": "a1871c990141b5456e302f44eb5ea42947da3d0e45311718927856de2996d421", + "bytes": 3352344, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm100.so" + }, + { + "native_abi": 1, + "arch": 120, + "sha256": "66220a378c062f1efe465f8ea1bc171806c397701b2c50c9a4a13cd1c86ed8ad", + "bytes": 3405592, + "gpu_runtime_validation": false, + "platform": "Linux", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm120.so" + }, + { + "native_abi": 1, + "arch": 86, + "sha256": "50b4446bfeaff14d320afec83e1b16b2ad299f237d9bb26d0d56807f28926b22", + "bytes": 2281472, + "gpu_runtime_validation": false, + "platform": "Windows", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm86.dll" + }, + { + "native_abi": 1, + "arch": 89, + "sha256": "dd20b03c7dbe339e235d650b44578446e8607939469328b5b6c161ba33ea9668", + "bytes": 2134016, + "gpu_runtime_validation": false, + "platform": "Windows", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm89.dll" + }, + { + "native_abi": 1, + "arch": 120, + "sha256": "1a67a3f853506dec4b5418fc2dbc00abcf4ba1ee8023e487bb2ecd48422bee3a", + "bytes": 2384384, + "gpu_runtime_validation": false, + "platform": "Windows", + "cuda": "13.0", + "source_commit": "0413d43878606425a84aa400f1e5da50ed9dcdad", + "file": "libck_temporal_sm120.dll" + } + ] +} diff --git a/minimax_h3_temporal_bootstrap.py b/minimax_h3_temporal_bootstrap.py index b9d1ee5..e6f87d1 100644 --- a/minimax_h3_temporal_bootstrap.py +++ b/minimax_h3_temporal_bootstrap.py @@ -1,27 +1,30 @@ -"""Pinned, node-local bootstrap for the optional native temporal extension. +"""Pinned platform/GPU-specific bootstrap for the optional temporal extension. -Only fixed trusted-release bytes are loaded, after embedded SHA256 checks. -No pip, site-packages writes, main Kitchen replacement, or sampling-time build. +Verified release bytes only. No pip, compilation, site-packages writes or Kitchen +replacement. Build/ABI checks do not imply GPU runtime validation. """ import ctypes import hashlib import importlib.util +import json import os from pathlib import Path import platform import sys import tempfile import threading -import time import urllib.request +try: + from .minimax_h3_temporal_lock import file_lock +except ImportError: + from minimax_h3_temporal_lock import file_lock -SOURCE_COMMIT = '9953b6caec7a5cb3e2486dd97a365f7af44eedad' +MANIFEST = json.loads((Path(__file__).parent/'h3_temporal_binaries.json').read_text()) +SOURCE_COMMIT = MANIFEST['source_commit'] RAW = f'https://raw.githubusercontent.com/alisson-anjos/comfy-kitchen/{SOURCE_COMMIT}' -RELEASE = 'https://github.com/alisson-anjos/comfy-kitchen/releases/download/h3-native-temporal-kernel-v1' -BINARY_HASH = 'ad87534bfb3ece2af84c201e326b2b405f0108f34efb36937c09967c14f0329d' -ASSETS = ( - ('temporal_attention.py', RAW+'/comfy_kitchen/temporal_attention.py', '0151e8b5b5886c9e8b4608cbc179e9dd6f934170cf45b8d27e14801f3c4828a1', 256*1024), - ('backends/cuda/native_temporal/libck_temporal_sm120.so', RELEASE+'/libck_temporal_sm120.so', BINARY_HASH, 16*1024*1024), +RELEASE = 'https://github.com/alisson-anjos/comfy-kitchen/releases/download/'+MANIFEST['release_tag'] +COMMON_ASSETS = ( + ('temporal_attention.py', RAW+'/comfy_kitchen/temporal_attention.py', '756171846469864702903d19e86a5f2b793a1e01b8fab57a72c68306dd1f2ee5', 256*1024), ('LICENSE', RAW+'/LICENSE', 'cd1c9ea1573c9047e9b5a3676ecc1c8f755e56f249ac1ebd8b5ceda7899037da', 64*1024), ('NOTICE', RAW+'/NOTICE', '35c43127ad3acbd6bc94ff63c56684a7c26eedf07795e596c8105a0476dae2ea', 64*1024), ) @@ -30,32 +33,54 @@ _READY = {} _FAILED = set() -def cache_root(): +def select_binary(system, machine, capability): + if system not in ('Linux','Windows') or machine.lower() not in ('x86_64','amd64'): + return None + arch = int(capability[0])*10+int(capability[1]) + return next((b for b in MANIFEST['binaries'] if b['platform']==system and b['arch']==arch),None) + + +def binary_for_device(device): + import torch + return select_binary(platform.system(),platform.machine(),torch.cuda.get_device_capability(device)) + + +def cache_base(): explicit=os.environ.get('BFS_H3_TEMPORAL_CACHE_DIR') - if explicit: - base=Path(explicit).expanduser() - else: - try: - import folder_paths - base=Path(folder_paths.get_user_directory())/'bfs'/'h3_temporal_kernels' - except (ImportError,AttributeError): - base=Path.home()/'.cache'/'bfsnodes'/'h3_temporal_kernels' - return base/('v1-linux-x86_64-sm120-'+BINARY_HASH[:12]) + if explicit:return Path(explicit).expanduser() + try: + import folder_paths + return Path(folder_paths.get_user_directory())/'bfs'/'h3_temporal_kernels' + except (ImportError,AttributeError): + return Path.home()/'.cache'/'bfsnodes'/'h3_temporal_kernels' + + +def cache_root(target): + return cache_base()/(f"v1-{target['platform'].lower()}-x86_64-sm{target['arch']}-"+target['sha256'][:12]) + + +def driver_version(): + driver=ctypes.WinDLL('nvcuda.dll') if platform.system()=='Windows' else ctypes.CDLL('libcuda.so.1') + version=ctypes.c_int() + if driver.cuDriverGetVersion(ctypes.byref(version))!=0: + raise RuntimeError('Cannot query NVIDIA CUDA driver version') + return version.value def compatible(device): if os.environ.get('BFS_H3_TEMPORAL_AUTO_INSTALL','1').lower() in ('0','false','off'): return False,'automatic setup disabled' - if platform.system()!='Linux' or platform.machine() not in ('x86_64','AMD64'): - return False,'automatic prebuilt supports Linux x86_64 only' + if platform.system() not in ('Linux','Windows') or platform.machine().lower() not in ('x86_64','amd64') or ctypes.sizeof(ctypes.c_void_p)!=8: + return False,'automatic prebuilts require 64-bit Linux or Windows on x86_64' import torch - if getattr(torch.version,'hip',None) or not torch.cuda.is_available() or torch.cuda.get_device_capability(device)!=(12,0): - return False,'automatic prebuilt supports NVIDIA SM120 only' - # Check the actual driver, not torch.version.cuda: the sidecar uses its own - # static CUDA runtime and the same device/stream primary context. - driver=ctypes.CDLL('libcuda.so.1');version=ctypes.c_int() - if driver.cuDriverGetVersion(ctypes.byref(version))!=0 or version.value<13000: - return False,'the prebuilt requires a driver supporting CUDA 13' + if getattr(torch.version,'hip',None) or not torch.cuda.is_available(): + return False,'automatic prebuilts require NVIDIA CUDA' + target=binary_for_device(device) + if target is None: + return False,'no pinned prebuilt for this operating system/GPU architecture' + # The sidecar uses CUDA 13's static runtime; PyTorch itself may use an older CUDA. + if driver_version()limit:raise RuntimeError('Temporal asset exceeds the pinned size limit') h.update(block);out.write(block) - if h.hexdigest()!=expected: - raise RuntimeError('Temporal asset checksum mismatch; downloaded code was not loaded') + if h.hexdigest()!=expected:raise RuntimeError('Temporal asset checksum mismatch; downloaded code was not loaded') os.replace(temp,path) finally: if temp.exists():temp.unlink() -def provision(root): - import fcntl - root.mkdir(parents=True,exist_ok=True) - with (root/'.install.lock').open('a+b') as lock: - deadline=time.monotonic()+60 - while True: - try: - fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);break - except BlockingIOError: - if time.monotonic()>deadline:raise RuntimeError('Timed out waiting for temporal setup lock') - time.sleep(.1) - missing=any(not (root/rel).is_file() or digest(root/rel)!=sha for rel,_,sha,_ in ASSETS) +def assets_for(target): + binary=('backends/cuda/native_temporal/'+target['file'],RELEASE+'/'+target['file'],target['sha256'],target['bytes']) + return (*COMMON_ASSETS,binary) + + +def provision(root,target): + with file_lock(root/'.install.lock',60): + assets=assets_for(target) + missing=any(not (root/rel).is_file() or digest(root/rel)!=sha for rel,_,sha,_ in assets) if missing: - print('[BFS H3 native temporal] First-use setup: downloading the verified one-pass extension (~3.3 MB). No compilation or pip install.',flush=True) - for rel,url,sha,limit in ASSETS: + print(f"[BFS H3 native temporal] First-use setup: downloading verified {target['platform']} SM{target['arch']} one-pass extension ({target['bytes']/1e6:.1f} MB). No compilation or pip install.",flush=True) + # Verify the binary before importing any downloaded Python code. + for rel,url,sha,limit in sorted(assets,key=lambda a:a[0].endswith('.py')): download(root/rel,url,sha,limit) - # Verify the native binary before executing the Python entry point too. - binary=root/ASSETS[1][0];lib=ctypes.CDLL(str(binary)) - lib.ck_temporal_abi_version.restype=ctypes.c_int;lib.ck_temporal_arch.restype=ctypes.c_int - if lib.ck_temporal_abi_version()!=1 or lib.ck_temporal_arch()!=120: - raise RuntimeError('Temporal prebuilt ABI/architecture mismatch') - name='_bfs_ck_temporal_v1_sm120_'+ASSETS[0][2][:12]+'_'+hashlib.sha256(str(root.resolve()).encode()).hexdigest()[:12] - if name in sys.modules: - return sys.modules[name] - spec=importlib.util.spec_from_file_location(name,root/'temporal_attention.py') - module=importlib.util.module_from_spec(spec);sys.modules[name]=module - try:spec.loader.exec_module(module) - except BaseException: - sys.modules.pop(name,None);raise - if getattr(module,'VERSION',None)!='h3_native_temporal_kernel_v1': - sys.modules.pop(name,None);raise RuntimeError('Temporal API version mismatch') - print(f'[BFS H3 native temporal] Verified one-pass extension ready: {root}',flush=True) - return module + return load_api(root,target) + + +def load_api(root,target): + binary=root/'backends/cuda/native_temporal'/target['file'] + lib=ctypes.CDLL(str(binary)) + lib.ck_temporal_abi_version.restype=ctypes.c_int;lib.ck_temporal_arch.restype=ctypes.c_int + if lib.ck_temporal_abi_version()!=target['native_abi'] or lib.ck_temporal_arch()!=target['arch']: + raise RuntimeError('Temporal native ABI/architecture mismatch') + name=f"_bfs_ck_temporal_v1_sm{target['arch']}_"+COMMON_ASSETS[0][2][:12]+'_'+hashlib.sha256(str(root.resolve()).encode()).hexdigest()[:12] + if name in sys.modules:return sys.modules[name] + spec=importlib.util.spec_from_file_location(name,root/'temporal_attention.py') + module=importlib.util.module_from_spec(spec);sys.modules[name]=module + try: + spec.loader.exec_module(module) + if getattr(module,'VERSION',None)!='h3_native_temporal_kernel_v1':raise RuntimeError('Temporal API version mismatch') + except BaseException: + sys.modules.pop(name,None);raise + print(f"[BFS H3 native temporal] Verified {target['platform']} SM{target['arch']} one-pass extension ready: {root}",flush=True) + return module def ensure(device): - """At most one automatic setup attempt per process/cache location.""" - key=(str(cache_root()),str(device)) + """At most one automatic setup attempt per device/cache location per process.""" + key=(str(cache_base()),platform.system(),str(device)) with _LOCK: if key in _READY:return _READY[key] if key in _FAILED:return None @@ -142,11 +166,14 @@ def ensure(device): _FAILED.add(key) print(f'[BFS H3 native temporal] Automatic native setup skipped: {reason}; retaining the existing backend.',flush=True) return None - module=provision(Path(key[0])) + target=binary_for_device(device) + if target is None:raise RuntimeError('No pinned native temporal prebuilt for this device') + module=provision(cache_root(target),target) if not module.is_available(device):raise RuntimeError('Verified native extension is unavailable for this device') _READY[key]=module return module except Exception as exc: + if type(exc).__name__=='InterruptProcessingException':raise _FAILED.add(key) print(f'[BFS H3 native temporal] Automatic native setup unavailable: {exc}. Retaining the existing backend. Restart ComfyUI to retry, or install the extension manually.',flush=True) return None diff --git a/minimax_h3_temporal_lock.py b/minimax_h3_temporal_lock.py new file mode 100644 index 0000000..2e7d698 --- /dev/null +++ b/minimax_h3_temporal_lock.py @@ -0,0 +1,28 @@ +"""Per-cache process lock on Linux and Windows.""" +from contextlib import contextmanager +import platform +import time + +@contextmanager +def file_lock(path,timeout=60): + path.parent.mkdir(parents=True,exist_ok=True) + with path.open('a+b') as handle: + windows=platform.system()=='Windows' + if windows: + import msvcrt + handle.seek(0,2) + if handle.tell()==0:handle.write(b'\0');handle.flush() + def acquire():handle.seek(0);msvcrt.locking(handle.fileno(),msvcrt.LK_NBLCK,1) + def release():handle.seek(0);msvcrt.locking(handle.fileno(),msvcrt.LK_UNLCK,1) + else: + import fcntl + def acquire():fcntl.flock(handle,fcntl.LOCK_EX|fcntl.LOCK_NB) + def release():fcntl.flock(handle,fcntl.LOCK_UN) + deadline=time.monotonic()+timeout + while True: + try:acquire();break + except OSError: + if time.monotonic()>deadline:raise RuntimeError('Timed out waiting for temporal kernel setup') + time.sleep(.1) + try:yield + finally:release() diff --git a/pyproject.toml b/pyproject.toml index 24f4592..e4ba9af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bfsnodes" description = "Custom ComfyUI nodes for reference-guided video: BFS Sampler (guide-driven IC-LoRAs, auto crop, mask inpainting, temporal chunking), LTX Multiple Controls / Identity Transfer / Edit Anything, Head-Swap and Anime2Real conditioning, MiniMax-H3 latent guides, RoPE layouts and loaders, LoRA Surgery, and Shot Planner / Shot Loop (tabbed panel): split long videos into shots at camera cuts, face-tracked cast, SAM 3 mask per shot with full frame, paste, mask, crop and crop + mask generation modes, optional translucent Colored Mark overlay for manually selected regions, optional Qwen3-VL to suggest settings, describe references ({details}) and the person to replace ({target}), continuity between shots, H3 conditioning with an optional duet and prompt writer, join with the original timing, audio and a side-by-side comparison. H3 Duet: pin a source clip beside the video, training-free." -version = "1.115.21" +version = "1.115.22" license = {file = "LICENSE"} dependencies = [ "torch", diff --git a/tests/test_minimax_h3_temporal_bootstrap.py b/tests/test_minimax_h3_temporal_bootstrap.py index 23dba65..8af2128 100644 --- a/tests/test_minimax_h3_temporal_bootstrap.py +++ b/tests/test_minimax_h3_temporal_bootstrap.py @@ -16,8 +16,9 @@ def setup(monkeypatch,tmp_path): boot._READY.clear();boot._FAILED.clear() source=b"VERSION='h3_native_temporal_kernel_v1'\ndef is_available(device):return True\n" binary=b'fake verified native fixture' - assets=[('temporal_attention.py','https://test.invalid/api',hashlib.sha256(source).hexdigest(),1024),('backends/cuda/native_temporal/libck_temporal_sm120.so','https://test.invalid/bin',hashlib.sha256(binary).hexdigest(),1024)] - monkeypatch.setattr(boot,'ASSETS',tuple(assets)) + target=dict(platform='Linux',arch=120,file='libck_temporal_sm120.so',native_abi=1,sha256=hashlib.sha256(binary).hexdigest(),bytes=len(binary)) + monkeypatch.setattr(boot,'COMMON_ASSETS',(('temporal_attention.py','https://test.invalid/api',hashlib.sha256(source).hexdigest(),1024),)) + monkeypatch.setattr(boot,'binary_for_device',lambda d:target) requests=[] def urlopen(request,timeout): requests.append(request.full_url) @@ -26,10 +27,10 @@ def setup(monkeypatch,tmp_path): loads=[] def cdll(path): loads.append(path) - return SimpleNamespace(ck_temporal_abi_version=NativeFunction(1),ck_temporal_arch=NativeFunction(120)) + return SimpleNamespace(ck_temporal_abi_version=NativeFunction(1),ck_temporal_arch=NativeFunction(target['arch'])) monkeypatch.setattr(boot.ctypes,'CDLL',cdll) monkeypatch.setattr(boot,'compatible',lambda d:(True,None)) - yield SimpleNamespace(root=boot.cache_root(),requests=requests,loads=loads,source=source,binary=binary) + yield SimpleNamespace(root=boot.cache_root(target),target=target,requests=requests,loads=loads,source=source,binary=binary) boot._READY.clear();boot._FAILED.clear() @@ -103,3 +104,53 @@ def test_bad_binary_arch_is_rejected_before_python_api_execution(setup,monkeypat monkeypatch.setattr(boot.ctypes,'CDLL',lambda p:SimpleNamespace(ck_temporal_abi_version=NativeFunction(1),ck_temporal_arch=NativeFunction(89))) assert boot.ensure('cuda:0') is None assert not [n for n in set(sys.modules)-before if n.startswith('_bfs_ck_temporal')] + + +@pytest.mark.parametrize('system,arch',[(b['platform'],b['arch']) for b in boot.MANIFEST['binaries']]) +def test_release_matrix_selects_exact_os_and_arch(system,arch): + target=boot.select_binary(system,'AMD64' if system=='Windows' else 'x86_64',(arch//10,arch%10)) + assert target['arch']==arch and target['platform']==system + assert target['file'].endswith('.dll' if system=='Windows' else '.so') + assert len(target['sha256'])==64 and target['bytes']>0 and target['native_abi']==1 + assert target['gpu_runtime_validation'] is False + + +@pytest.mark.parametrize('system,machine,cap',[('Darwin','x86_64',(12,0)),('Linux','aarch64',(12,0)),('Windows','AMD64',(9,0)),('Linux','x86_64',(7,5))]) +def test_unpublished_combinations_never_substitute_another_binary(system,machine,cap): + assert boot.select_binary(system,machine,cap) is None + + +def test_windows_ada_downloads_only_its_own_dll(setup,monkeypatch): + from contextlib import nullcontext + setup.target.update(platform='Windows',arch=89,file='libck_temporal_sm89.dll') + monkeypatch.setattr(boot.platform,'system',lambda:'Windows') + monkeypatch.setattr(boot,'file_lock',lambda *a,**k:nullcontext()) + module=boot.ensure('cuda:0') + assert module is not None + assert len(setup.requests)==2 + assert any(url.endswith('libck_temporal_sm89.dll') for url in setup.requests) + assert not any('sm120' in url or url.endswith('.so') for url in setup.requests) + assert (boot.cache_root(setup.target)/'backends/cuda/native_temporal/libck_temporal_sm89.dll').is_file() + + +def test_platform_and_arch_produce_separate_cache_names(setup): + linux=boot.cache_root(setup.target) + windows=boot.cache_root(dict(setup.target,platform='Windows')) + ada=boot.cache_root(dict(setup.target,arch=89)) + assert len({linux,windows,ada})==3 + + +def test_automatic_setup_never_launches_a_compiler_or_pip(setup,monkeypatch): + import subprocess + def forbidden(*a,**kw):raise AssertionError('automatic download must not launch a build or pip') + monkeypatch.setattr(subprocess,'Popen',forbidden) + assert boot.ensure('cuda:0') is not None + + +def test_windows_driver_query_uses_nvcuda_not_linux_lib(monkeypatch): + seen=[] + class Driver: + def cuDriverGetVersion(self,p):p._obj.value=13000;return 0 + monkeypatch.setattr(boot.platform,'system',lambda:'Windows') + monkeypatch.setattr(boot.ctypes,'WinDLL',lambda name:(seen.append(name) or Driver()),raising=False) + assert boot.driver_version()==13000 and seen==['nvcuda.dll'] diff --git a/tests/test_minimax_h3_temporal_lock.py b/tests/test_minimax_h3_temporal_lock.py new file mode 100644 index 0000000..fac8ff7 --- /dev/null +++ b/tests/test_minimax_h3_temporal_lock.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path +from types import SimpleNamespace +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +import minimax_h3_temporal_lock as locks + + +def test_windows_locks_one_existing_byte(tmp_path,monkeypatch): + calls=[] + monkeypatch.setattr(locks.platform,'system',lambda:'Windows') + monkeypatch.setitem(sys.modules,'msvcrt',SimpleNamespace(LK_NBLCK=1,LK_UNLCK=2,locking=lambda fd,mode,size:calls.append((mode,size)))) + path=tmp_path/'cache.lock' + with locks.file_lock(path):assert path.stat().st_size>=1 + assert calls==[(1,1),(2,1)] + + +def test_linux_lock_acquires_and_releases(tmp_path): + with locks.file_lock(tmp_path/'cache.lock'):pass + with locks.file_lock(tmp_path/'cache.lock'):pass