Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions freevideo_engine/adaln.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ def __init__(self, root, source_id, steps, task='t2va', *, manifest=None,
self.optional_download_bytes = 0
self.root = Path(root) / assets.directory(self.identity)
self.producer = None
self.shared = None
if self.asset is None:
from .paths import model_root
# Published tables installed by setup or the request preflight.
from .paths import installed_model_root
from .sampling_assets import cache_root
shared = cache_root(model_root()) / assets.directory(self.identity)
if shared.is_dir():
self.root = shared
self.shared = cache_root(installed_model_root()) / assets.directory(self.identity)
if self.asset is not None:
if self.asset['directory'] != self.root.name:
raise ValueError('AdaLN asset identity/path mismatch')
Expand Down Expand Up @@ -126,14 +126,18 @@ def load(self, index, steps):
raise ValueError('AdaLN cache schedule length mismatch')
path = self.root / f'{index:02d}.safetensors'
marker = path.with_suffix('.json')
if self.asset is None and not (path.is_file() and marker.is_file()) and self.shared is not None:
path = self.shared / path.name
marker = path.with_suffix('.json')
if self.asset is not None:
row = next((r for r in self.asset['files'] if r['index'] == index), None)
if row is None:
raise ValueError('Incomplete AdaLN model asset')
elif not path.is_file() or not marker.is_file():
if self.optional_asset is None:
return None
row = assets.download_table(self.root, self.optional_asset, index)
path.parent.mkdir(parents=True, exist_ok=True)
row = assets.download_table(path.parent, self.optional_asset, index)
self.optional_downloaded.add(index)
self.optional_download_bytes += row['bytes']
else:
Expand Down
7 changes: 4 additions & 3 deletions freevideo_engine/adaln_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,20 @@ def optional_table(expected, manifest):
return dict(table, download=dict(repo=optional['repo'], revision=optional['revision'], prefix=prefix))


def _download_plan():
def _download_plan(root=None):
from . import network
from .paths import data_root
root = Path(root) if root is not None else data_root()
plan = network.installed_plan()
machine = data_root() / 'machine.json'
machine = root / 'machine.json'
if not plan and machine.is_file():
installed = json.loads(machine.read_text(encoding='utf-8'))
plan = installed.get('network', {}) or {}
if not plan and installed.get('setup_run'):
setup = Path(installed['setup_run']) / 'plan.json'
if setup.is_file():
plan = json.loads(setup.read_text(encoding='utf-8')).get('network', {}) or {}
plan.setdefault('download_settings_path', str(data_root() / 'download-settings.json'))
plan.setdefault('download_settings_path', str(root / 'download-settings.json'))
plan.setdefault('sources', {}).setdefault('models', [{'id': 'official'}, {'id': 'hf-mirror'}])
return plan

Expand Down
18 changes: 13 additions & 5 deletions freevideo_engine/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,16 @@ def plan(args, *, local_progress=None):
errors.append('No verified native-compatible prepared model is available. Source conversion is not supported on Mac.')
files = required_models(json.loads((PACKAGE / 'model_files.json').read_text(encoding='utf-8')), reuse_cache or prepared)
files += prepared_model.files(prepared)
sampling_caches = bool(getattr(args, 'sampling_caches', False))
if sampling_caches:
from .sampling_assets import files as sampling_files
files += sampling_files()
# New installations prepare every quality level; an existing one keeps its
# earlier choice (off if it predates the option) unless the flag says otherwise.
sampling_caches = getattr(args, 'sampling_caches', None)
if sampling_caches is None:
sampling_caches = prior['sampling_caches'] if isinstance(prior.get('sampling_caches'), bool) else not saved.get('ready')
sampling_caches = bool(sampling_caches)
from .sampling_assets import install_files, usable_with
if prepared or (reuse_cache and usable_with(reuse_cache)):
# Published tables match these weights; other caches compute their own.
files += install_files(sampling_caches)
local_reuse = None
local_folder = getattr(args, 'reuse_models', None)
local_manifest = getattr(args, 'reuse_models_manifest', None)
Expand Down Expand Up @@ -1419,7 +1425,9 @@ def main(argv=None):
parser.add_argument('--environment', choices=('unified', 'dual'),
help='New installs default to unified; updates retain their saved layout. Existing environments are kept when switching.')
parser.add_argument('--models', type=Path, help='Reuse/download official model files in this directory')
parser.add_argument('--sampling-caches', action='store_true', help='Install all four quality levels and reference-mode sampling caches in advance')
parser.add_argument('--sampling-caches', action=argparse.BooleanOptionalAction, default=None,
help='Install every quality level in advance (default for new installations; existing ones keep '
'their choice). The default 8 + 3 refinement tables are always installed.')
parser.add_argument('--model-source', choices=('prepared', 'source'), default='prepared',
help='Default: download a pinned slim model matching the GPU format. source: explicitly download original weights and convert locally')
parser.add_argument('--encoder-models', type=Path, help='Directory containing text_encoders/')
Expand Down
5 changes: 3 additions & 2 deletions freevideo_engine/comfy_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,13 +563,14 @@ def inspect_saved(check):
send_progress({'label': 'Reused previous result', 'phase': 'complete',
'result_cache_hit': True})
return reused
from .sampling_assets import prepare as prepare_sampling_assets
from .sampling_assets import engine_task, prepare as prepare_sampling_assets
def asset_progress(message):
if progress:
progress(dict(message, report_id=report_id))
preparation_started = time.monotonic()
try:
preparation = prepare_sampling_assets(root, machine, planned, task_for(extra.get('media', {})),
# Reference audio selects its own tables; match the encoder's choice.
preparation = prepare_sampling_assets(root, machine, planned, engine_task(extra.get('media', {}), run),
progress=asset_progress, interrupted=interrupted, environ=environment)
except BaseException:
elapsed = time.monotonic() - preparation_started
Expand Down
10 changes: 6 additions & 4 deletions freevideo_engine/comfy_launcher_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,11 +492,13 @@ def _inspect(self, values):
self.setup.runner.token = validate(values.get('token', ''))
url = local_url(values.get('url', ''))
ready = False
if not values.get('repair') and not values.get('sampling_caches'):
if not values.get('repair'):
try:
installation(source, {'FREEVIDEO_HOME': str(engine)})
ready = True
except (OSError, ValueError):
_, machine = installation(source, {'FREEVIDEO_HOME': str(engine)})
# With every quality level requested, set up again only while some are missing.
from .sampling_assets import installed as sampling_installed
ready = not values.get('sampling_caches') or sampling_installed(machine)
except (OSError, ValueError, KeyError):
pass
self.selection = dict(descriptor, engine=str(engine), source=str(source), url=url, ready=ready)
self.state = dict(self.state, selection=dict(self.selection), host=host)
Expand Down
3 changes: 2 additions & 1 deletion freevideo_engine/comfy_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ async def sampling_estimate(request):
canvas = geometry(int(request.query['width']), int(request.query['height']),
seconds=float(request.query['seconds']))
_, machine = installation()
rows = await asyncio.to_thread(local_records, folder_paths.get_output_directory(), machine.get('gpu_uuid'))
device = machine.get('device_identity') if machine.get('device_backend') == 'mps' else machine.get('gpu_uuid')
rows = await asyncio.to_thread(local_records, folder_paths.get_output_directory(), device)
task = request.query.get('task', 't2va')
adapters = request.query.get('adapters') == '1'
result = {name: {str(steps): estimate(rows, canvas, base_steps=steps, two_pass=enabled,
Expand Down
4 changes: 2 additions & 2 deletions freevideo_engine/comfy_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ def inspect(self, value):
arguments.append('--frontend-separate')
if frontend.get('download'):
arguments.append('--frontend-download')
if value.get('sampling_caches') is True:
arguments.append('--sampling-caches')
if isinstance(value.get('sampling_caches'), bool):
arguments.append('--sampling-caches' if value['sampling_caches'] else '--no-sampling-caches')
if value.get('copy'):
arguments.append('--copy-existing-models')
self.selection = dict(root=str(root), arguments=arguments)
Expand Down
5 changes: 3 additions & 2 deletions freevideo_engine/diagnostic_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,16 @@ def summary_report(report):
if isinstance(shape, (list, tuple)) and len(shape) == 2 and all(type(x) is int and x > 0 for x in shape):
result['geometry']['text_tokens'] = shape[0]
for k in ('resident_blocks','pin_host_gb','head_chunk','window_batch','head_parallelism','ff_chunk','projection_chunk',
'query_chunk','fp8_linears','resident_weight_bytes','pinned_model_bytes','pinned_host_allocated_bytes','steps'):
'query_chunk','fp8_linears','resident_weight_bytes','pinned_model_bytes','pinned_host_allocated_bytes','steps',
'adaln_optional_blocks','adaln_downloaded_files','adaln_downloaded_bytes'):
if _number(config.get(k)) is not None:
result['config'][k] = config[k]
for k in ('prefetch','stream_weights','attention_cpu_outputs','grouped_attention_outputs','residual_offload','fp8_ff_recompute',
'inference_kernels','window_varlen','varlen_smooth_k','reuse_block_outputs','preload_host','pin_host_weights','cache_refined_text'):
if type(config.get(k)) is bool:
result['config'][k] = config[k]
for k, allowed in (('task', ('t2va','fl2va','ref2va')), ('linear_compute',('native-fp8','bf16-weight-only')),
('adaln_mode', ('portable-model-asset','local-precompute','original-projections'))):
('adaln_mode', ('portable-model-asset','optional-model-asset','local-precompute','original-projections'))):
if config.get(k) in allowed:
result['config'][k] = config[k]
for k, allowed in (('fp8_gemm', ('torch','scaled-mm-epilogue','triton')),
Expand Down
13 changes: 11 additions & 2 deletions freevideo_engine/effort_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ def positive(value):
return type(value) in (float, int) and math.isfinite(value) and value > 0


def same_device(hardware, device):
"""NVIDIA GPUs match by UUID; Macs by chip and unified memory (no UUID)."""
if isinstance(device, dict):
return (device.get('backend') == 'mps' and hardware.get('device_backend') == 'mps'
and bool(device.get('name')) and hardware.get('gpu_name') == device['name']
and hardware.get('ram_total') == device.get('unified_ram_bytes'))
return bool(device) and hardware.get('gpu_uuid') == device


def timing_record(report, gpu_uuid, system):
if not isinstance(report, dict) or report.get('success') is not True:
return None
Expand All @@ -25,7 +34,7 @@ def timing_record(report, gpu_uuid, system):
or video.get('first_pass_reused') or video.get('phase') != 'complete'):
return None
hardware = report.get('profile', {}).get('policy', {}).get('hardware', {})
if not gpu_uuid or hardware.get('gpu_uuid') != gpu_uuid or hardware.get('system') != system:
if not same_device(hardware, gpu_uuid) or hardware.get('system') != system:
return None
shape = report.get('geometry', {})
plan = report.get('sampling_plan') or shape.get('sampling_plan') or {}
Expand Down Expand Up @@ -119,7 +128,7 @@ def local_records(output_directory, gpu_uuid, *, system=None):
"""Bounded recent JSON reads, cached between slider changes; never open MP4s."""
system = system or platform.system()
root = Path(output_directory).resolve() / 'FreeVideo'
key = (str(root), gpu_uuid, system)
key = (str(root), json.dumps(gpu_uuid, sort_keys=True), system)
now = time.monotonic()
cached = _CACHE.get(key)
if cached and now - cached[0] < 15:
Expand Down
2 changes: 1 addition & 1 deletion freevideo_engine/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ def interrupted(signum, frame):
tokens = next((row['observation'].get('text_tokens', row.get('geometry', {}).get('text_tokens'))
for row in reversed(observations)
if row['observation'].get('conditioning_sha256') == condition_hash), None)
if state and args.attention == 'auto' and tokens is not None and sampling_plan['version'] == 1:
if state and args.attention == 'auto' and tokens is not None and sampling_plan['version'] in (1, 3):
profile, applied = tuning.apply_profile(state, profile, canvas, tokens)
report['profile'] = profile
report['tuning']['profile_id'] = applied
Expand Down
3 changes: 2 additions & 1 deletion freevideo_engine/launcher_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ def __init__(self, source=None, *, controller=None, store=None, updater=None, sm
home = Path(os.environ.get('USERPROFILE') or os.environ.get('HOME') or launcher_root().parent)
self.form = dict(comfy='', destination=str(home / 'FreeVideo'), engine='', python='',
url='http://127.0.0.1:8188', models='', model_dirs=[], model_method='auto', environment_method='auto',
separate=False, repair=False, new_comfy=True, offline_runtime='', offline_models=[], sampling_caches=False)
separate=False, repair=False, new_comfy=True, offline_runtime='', offline_models=[],
sampling_caches=not saved.get('installation'))
self.form.update({k: v for k, v in saved.items() if k in self.form})
if 'environment_method' not in saved and self.form['offline_runtime']:
self.form['environment_method'] = 'manual'
Expand Down
16 changes: 16 additions & 0 deletions freevideo_engine/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ def model_root():
return Path(os.environ.get('FREEVIDEO_MODEL_ROOT', data_root() / 'models')).expanduser()


def installed_model_root():
"""The model folder setup recorded (models/vdn), unless explicitly overridden.

Engine processes only receive FREEVIDEO_HOME, where model_root() means
models/. Files setup downloads, such as the latent upscaler and sampling
tables, live in the recorded folder.
"""
if not os.environ.get('FREEVIDEO_MODEL_ROOT'):
import json
try:
return Path(json.loads((data_root() / 'machine.json').read_text(encoding='utf-8'))['model_root']).expanduser()
except (OSError, ValueError, KeyError, TypeError):
pass
return model_root()


def add_vdn():
root = vdn_root().resolve()
if not (root / 'src/models/hybrid_attention.py').is_file():
Expand Down
Loading