-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Studio: install flashinfer on demand for NVFP4 without moving torch #11730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: studio-nvfp4-kernels
Are you sure you want to change the base?
Changes from all commits
a527b6b
8bff995
89db625
a013923
edc1b41
804edab
b301c4a
4101a0c
aae80d4
915d0f7
6af4e68
90bc915
d013b50
766a216
dd105df
838bfc0
1f0fb82
256cf94
5221079
46cf5af
28e8255
bf80962
1a14dbd
0caa7cd
5b24f5b
5b6b074
4c4def9
0932f7d
be0b4ab
6e0663c
30b251a
dd7ec47
3c6db72
2dc7869
ca8ec12
227cc8f
1e0a63c
b5a327b
10ec2ad
cd031eb
4f10020
1c0598f
6564efc
cfba066
7b56e24
d452adf
2500bf8
5e12a53
6074d16
87abedd
0f24d52
e6acf2f
777e7b7
796be23
eac82e1
3519005
8015b7b
d3092ac
41b9935
75c2299
9a456c7
2254718
bd9f1cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -202,6 +202,8 @@ | |
| resident_bytes_from_declared, | ||
| resolve_dense_quant_candidate, | ||
| ) | ||
| from .diffusion_nvfp4_flag import nvfp4_diffusion_enabled | ||
| from .diffusion_nvfp4_install import nvfp4_backend_fields as _nvfp4_backend_fields | ||
| from .diffusion_transformer_quant import ( | ||
| TQ_AUTO, | ||
| TQ_NVFP4, | ||
|
|
@@ -3329,6 +3331,43 @@ def _prequant_source_hub_entry( | |
| ) | ||
| return None | ||
|
|
||
| def _nvfp4_checkpoint_will_load( | ||
| self, | ||
| fam: Any, | ||
| base: Optional[str], | ||
| path_override: Optional[str], | ||
| hf_token: Optional[str], | ||
| *, | ||
| local_files_only: bool = False, | ||
| loras: Any = None, | ||
| ) -> bool: | ||
| """Whether an NVFP4 load opens a pre-quantised checkpoint (the only FlashInfer path); unanswerable -> False.""" | ||
| if not nvfp4_diffusion_enabled(): | ||
| return False | ||
| try: | ||
| if _has_active_lora(loras): | ||
| return False | ||
| source = usable_prequant_source( | ||
| fam, TQ_NVFP4, path_override = path_override, base_repo = base | ||
| ) | ||
| if source is None: | ||
| return False | ||
| if getattr(source, "kind", None) != "repo": | ||
| return True | ||
| if prequant_checkpoint_cached(source, cache_dir = hub_cache_dir()): | ||
| return True | ||
| if local_files_only: | ||
| return False | ||
| return self._prequant_source_hub_entry(source, hf_token, scheme = TQ_NVFP4) is not None | ||
| except Exception as exc: # noqa: BLE001 -- a refused or unreachable listing is no checkpoint | ||
| logger.info( | ||
| "diffusion.nvfp4_install: no reachable NVFP4 checkpoint for %s, so the load runs on " | ||
| "torchao and FlashInfer is not installed (%s)", | ||
| base, | ||
| type(exc).__name__, | ||
| ) | ||
| return False | ||
|
|
||
| @staticmethod | ||
| def _estimate_download_bytes( | ||
| repo_id: str, | ||
|
|
@@ -4409,6 +4448,51 @@ def load_pipeline( | |
| _ensure_attention_backend_installed(preinstall_backend, logger) | ||
| except Exception: # noqa: BLE001 - the locked path re-resolves and validates | ||
| pass | ||
| # Install FlashInfer only when a pre-quantised checkpoint will load: the on-the-fly build is torchao. | ||
| if ( | ||
| dense_quant_supported_kind(kind) | ||
| and nvfp4_diffusion_enabled() | ||
| and TQ_NVFP4 | ||
| in ( | ||
| normalize_transformer_quant(transformer_quant), | ||
| _pipeline_prequant_planned, | ||
| ) | ||
| # A declined seed loads the released denoiser (torchao or no quant), so no checkpoint for FlashInfer. | ||
| and _pipeline_prequant_planned != PIPELINE_SEED_DECLINED | ||
| and ( | ||
| _pipeline_prequant_planned == TQ_NVFP4 | ||
| or self._nvfp4_checkpoint_will_load( | ||
| fam, | ||
| base, | ||
| transformer_prequant_path, | ||
| hf_token, | ||
| local_files_only = local_files_only, | ||
| loras = loras, | ||
| ) | ||
| ) | ||
| # The locked re-plan (after teardown) may drop the capacity-settled seed; ask it now with teardown memory credited. | ||
| and not ( | ||
| kind == "pipeline" | ||
| and _pipeline_prequant_planned == TQ_NVFP4 | ||
| and not self._seed_plan_stays_resident( | ||
| TQ_NVFP4, | ||
| target, | ||
| base, | ||
| fam, | ||
| memory_mode, | ||
| cpu_offload, | ||
| repo_id = repo_id, | ||
| base_local_dir = _base_local_dir, | ||
| fetch_base = fetch_base, | ||
| ) | ||
| ) | ||
| ): | ||
| from .diffusion_nvfp4_install import ensure_flashinfer_for_nvfp4 | ||
| _nvfp4_install_outcome = ensure_flashinfer_for_nvfp4( | ||
| device, logger = logger, local_files_only = local_files_only | ||
| ) | ||
|
Comment on lines
+4491
to
+4493
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an image NVFP4 load is superseded or unloaded while this call is downloading the roughly 1.5 GB JIT cache, cancellation is not checked until Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving this as is. The install runs once per process under the install lock, rolls back on failure, and only adds the package the next NVFP4 load wants anyway, so a superseded load finishing it does no harm; the newer load's own ensure call sees the finished outcome. Killing pip mid-install would be the riskier path for the environment. |
||
| else: | ||
| _nvfp4_install_outcome = None | ||
|
|
||
| with self._lock: | ||
| self._raise_if_load_cancelled(_load_token) | ||
|
|
@@ -4424,6 +4508,10 @@ def load_pipeline( | |
| self._unload_locked() | ||
| finally: | ||
| self._release_teardown_locked() | ||
| # Bind even when the gate skipped, so no stale install reason survives. | ||
| from .diffusion_nvfp4_install import record_install_reason | ||
|
|
||
| record_install_reason(self, *(_nvfp4_install_outcome or (True, None)), device) | ||
|
|
||
| # Single-file kinds resolve a checkpoint path; the pipeline kind has none. | ||
| single_file_path = ( | ||
|
|
@@ -4462,29 +4550,16 @@ def load_pipeline( | |
| PIPELINE_SEED_DECLINED, | ||
| ): | ||
| pipeline_seed_scheme = _pipeline_prequant_planned | ||
| seed_estimate = estimate_dense_quant( | ||
| fam, pipeline_seed_scheme, base_repo = base, prequant_available = True | ||
| ) | ||
| seeded_plan = ( | ||
| self._plan_memory( | ||
| target, | ||
| single_file_path, | ||
| base, | ||
| fam, | ||
| memory_mode, | ||
| cpu_offload, | ||
| kind = kind, | ||
| repo_id = repo_id, | ||
| base_local_dir = _base_local_dir, | ||
| fetch_base = fetch_base, | ||
| transformer_resident_override_mib = ( | ||
| seed_estimate.steady_transformer_mib | ||
| ), | ||
| companion_override_mib = seed_estimate.companions_mib, | ||
| text_encoder_override_mib = seed_estimate.text_encoders_mib, | ||
| ) | ||
| if seed_estimate is not None | ||
| else None | ||
| seeded_plan = self._seeded_pipeline_plan( | ||
| pipeline_seed_scheme, | ||
| target, | ||
| base, | ||
| fam, | ||
| memory_mode, | ||
| cpu_offload, | ||
| repo_id = repo_id, | ||
| base_local_dir = _base_local_dir, | ||
| fetch_base = fetch_base, | ||
| ) | ||
| if seeded_plan is None or seeded_plan.offload_policy != OFFLOAD_NONE: | ||
| # Offload hooks use Module.to(), which torchao tensors reject, and live free | ||
|
|
@@ -6365,6 +6440,65 @@ def _precast_scaled_companions_mib( | |
| except Exception: # noqa: BLE001 -- sizing aid only; the dense total still refuses safely | ||
| return int(companions) | ||
|
|
||
| def _seeded_pipeline_plan( | ||
| self, | ||
| scheme: str, | ||
| target: Any, | ||
| base: str, | ||
| fam: Any, | ||
| memory_mode: Optional[str], | ||
| cpu_offload: bool, | ||
| *, | ||
| repo_id: str, | ||
| base_local_dir: Optional[str], | ||
| fetch_base: Optional[str], | ||
| device_memory_override: Optional[DeviceMemory] = None, | ||
| ): | ||
| """Full-pipeline plan priced on the pre-quantised ``scheme`` seed, or None when the family table cannot size it.""" | ||
| seed_estimate = estimate_dense_quant(fam, scheme, base_repo = base, prequant_available = True) | ||
| if seed_estimate is None: | ||
| return None | ||
| return self._plan_memory( | ||
| target, | ||
| None, | ||
| base, | ||
| fam, | ||
| memory_mode, | ||
| cpu_offload, | ||
| kind = "pipeline", | ||
| repo_id = repo_id, | ||
| base_local_dir = base_local_dir, | ||
| fetch_base = fetch_base, | ||
| transformer_resident_override_mib = seed_estimate.steady_transformer_mib, | ||
| companion_override_mib = seed_estimate.companions_mib, | ||
| text_encoder_override_mib = seed_estimate.text_encoders_mib, | ||
| device_memory_override = device_memory_override, | ||
| ) | ||
|
|
||
| def _seed_plan_stays_resident( | ||
| self, scheme: str, target: Any, *args: Any, **kwargs: Any | ||
| ) -> bool: | ||
| """Whether the post-teardown seeded plan keeps ``scheme``, asked before teardown with this process's allocation | ||
| credited as free (errs toward keeping the seed). Unanswerable keeps it.""" | ||
| try: | ||
| memory = snapshot_device_memory(target) | ||
| if getattr(target, "device", None) == "cuda" and memory.free_mib is not None: | ||
| import torch | ||
|
|
||
| free = int(memory.free_mib) + int(torch.cuda.memory_reserved()) // (1024 * 1024) | ||
| if memory.total_mib is not None: | ||
| free = min(free, int(memory.total_mib)) | ||
| memory = DeviceMemory( | ||
| memory.backend, memory.device, memory.memory_kind, free, memory.total_mib | ||
| ) | ||
| plan = self._seeded_pipeline_plan( | ||
| scheme, target, *args, device_memory_override = memory, **kwargs | ||
| ) | ||
| except Exception as exc: # noqa: BLE001 - a gate on an optional install must not fail the load | ||
| logger.debug("diffusion.nvfp4_install: seed plan preview skipped: %r", exc) | ||
| return True | ||
| return plan is not None and plan.offload_policy == OFFLOAD_NONE | ||
|
|
||
| def _resident_sized_plan( | ||
| self, | ||
| plan: Any, | ||
|
|
@@ -7974,6 +8108,7 @@ def status(self) -> dict[str, Any]: | |
| "text_encoder_quant": None, | ||
| "transformer_quant": None, | ||
| "transformer_quant_backend": None, | ||
| "transformer_quant_backend_reason": None, | ||
| "attention_backend": None, | ||
| "transformer_cache": None, | ||
| "workflows": [], | ||
|
|
@@ -8007,7 +8142,7 @@ def status(self) -> dict[str, Any]: | |
| "speed_optims": list(state.speed_optims), | ||
| "text_encoder_quant": state.text_encoder_quant, | ||
| "transformer_quant": state.transformer_quant, | ||
| "transformer_quant_backend": _transformer_quant_backend(state), | ||
| **_nvfp4_backend_fields(_transformer_quant_backend(state), owner = self), | ||
| "attention_backend": state.attention_backend, | ||
| "transformer_cache": state.transformer_cache, | ||
| "resolved": state.resolved, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an explicit image
transformer_quant="nvfp4"has_pipeline_prequant_planned == PIPELINE_SEED_DECLINED(for example, the artifact-sized prefetch plan still requires offload), the explicit request satisfies the earlier tuple check and this Hub probe can return true, so FlashInfer and its roughly 1.5 GB cache are installed. The loader later explicitly excludesPIPELINE_SEED_DECLINEDfrompipeline_seed_schemeat lines 4575-4578 and either performs the on-the-fly torchao conversion or declines quantization under offload, meaning FlashInfer cannot serve this load. Short-circuit this sentinel as the video gate already does before invoking the installer.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct: a declined seed loads the released denoiser, so the install bought nothing. Fixed in eac82e1, which skips the install when the plan declined the seed, with a regression test.