Performance fixes and bug fixes
This commit is contained in:
1 parent
5ddc5f8ca7
commit
7195abc32b
21 files changed
+511
-138
No files matched your search
@@ -206,11 +206,13 @@ class ChromaModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
|
text_encoder[1].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
text_encoder[1].to(self.device_torch)
|
|
||||||
text_encoder[1].requires_grad_(False)
|
text_encoder[1].requires_grad_(False)
|
||||||
text_encoder[1].eval()
|
text_encoder[1].eval()
|
||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|||||||
@@ -192,11 +192,13 @@ class ChromaRadianceModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
|
text_encoder[1].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
text_encoder[1].to(self.device_torch)
|
|
||||||
text_encoder[1].requires_grad_(False)
|
text_encoder[1].requires_grad_(False)
|
||||||
text_encoder[1].eval()
|
text_encoder[1].eval()
|
||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|||||||
@@ -133,8 +133,10 @@ class ErnieImageModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
flush()
|
flush()
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ class FLiteModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|||||||
@@ -98,12 +98,21 @@ class Flux2Model(BaseModel):
|
|||||||
self.print_and_status_update("Loading Mistral")
|
self.print_and_status_update("Loading Mistral")
|
||||||
|
|
||||||
# load + quantize + offload + placement, all driven by model_config
|
# load + quantize + offload + placement, all driven by model_config
|
||||||
|
# tie_word_embeddings=False: the checkpoint carries both embed_tokens
|
||||||
|
# and lm_head with different values; the config's tie claim is wrong
|
||||||
text_encoder = Mistral3TextEncoder.load(
|
text_encoder = Mistral3TextEncoder.load(
|
||||||
MISTRAL_PATH, subfolder="", **self.component_load_kwargs("te")
|
MISTRAL_PATH,
|
||||||
|
subfolder="",
|
||||||
|
tie_word_embeddings=False,
|
||||||
|
**self.component_load_kwargs("te"),
|
||||||
)
|
)
|
||||||
flush()
|
flush()
|
||||||
|
|
||||||
tokenizer = AutoProcessor.from_pretrained(MISTRAL_PATH)
|
# fix_mistral_regex=False: keep the exact tokenization flux2 has always
|
||||||
|
# used (True would change the pre-tokenizer and shift conditioning)
|
||||||
|
tokenizer = AutoProcessor.from_pretrained(
|
||||||
|
MISTRAL_PATH, fix_mistral_regex=False
|
||||||
|
)
|
||||||
return text_encoder, tokenizer
|
return text_encoder, tokenizer
|
||||||
|
|
||||||
def load_model(self):
|
def load_model(self):
|
||||||
|
|||||||
@@ -109,15 +109,19 @@ class Flux2Pipeline(DiffusionPipeline):
|
|||||||
# Process all messages at once
|
# Process all messages at once
|
||||||
# with image processing a too short max length can throw an error in here.
|
# with image processing a too short max length can throw an error in here.
|
||||||
try:
|
try:
|
||||||
|
# tokenization kwargs ride in processor_kwargs (same values end up
|
||||||
|
# in the same place; loose **kwargs just warn on new transformers)
|
||||||
inputs = self.tokenizer.apply_chat_template(
|
inputs = self.tokenizer.apply_chat_template(
|
||||||
messages_batch,
|
messages_batch,
|
||||||
add_generation_prompt=False,
|
add_generation_prompt=False,
|
||||||
tokenize=True,
|
tokenize=True,
|
||||||
return_dict=True,
|
return_dict=True,
|
||||||
return_tensors="pt",
|
return_tensors="pt",
|
||||||
padding="max_length",
|
processor_kwargs={
|
||||||
truncation=True,
|
"padding": "max_length",
|
||||||
max_length=max_sequence_length,
|
"truncation": True,
|
||||||
|
"max_length": max_sequence_length,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -137,11 +137,13 @@ class FluxKontextModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
|
text_encoder[1].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
text_encoder[1].to(self.device_torch)
|
|
||||||
text_encoder[1].requires_grad_(False)
|
text_encoder[1].requires_grad_(False)
|
||||||
text_encoder[1].eval()
|
text_encoder[1].eval()
|
||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|||||||
@@ -490,8 +490,10 @@ class LTX2Model(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
flush()
|
flush()
|
||||||
|
|||||||
@@ -144,8 +144,10 @@ class NucleusImageModel(QwenImageVAEHolderMixin, BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
flush()
|
flush()
|
||||||
|
|||||||
@@ -161,8 +161,10 @@ class QwenImageModel(QwenImageVAEHolderMixin, BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
flush()
|
flush()
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ class DualWanTransformer3DModel(torch.nn.Module):
|
|||||||
def dtype(self) -> torch.dtype:
|
def dtype(self) -> torch.dtype:
|
||||||
return self.torch_dtype
|
return self.torch_dtype
|
||||||
|
|
||||||
|
def get_offload_ignore_modules(self):
|
||||||
|
# both DiTs' fp32 modulation tables stay resident on the compute device
|
||||||
|
return (
|
||||||
|
self.transformer_1.get_offload_ignore_modules()
|
||||||
|
+ self.transformer_2.get_offload_ignore_modules()
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
return self.transformer_1.config
|
return self.transformer_1.config
|
||||||
@@ -128,17 +135,26 @@ class DualWanTransformer3DModel(torch.nn.Module):
|
|||||||
else:
|
else:
|
||||||
t_name = "transformer_2"
|
t_name = "transformer_2"
|
||||||
|
|
||||||
|
# memory-managed transformers own their placement (layers bounce
|
||||||
|
# from cpu per forward); whole-model swaps would haul the full
|
||||||
|
# 14B up and defeat the offloading
|
||||||
|
managed = (
|
||||||
|
hasattr(self, "_memory_manager")
|
||||||
|
or hasattr(self.transformer_1, "_memory_manager")
|
||||||
|
or hasattr(self.transformer_2, "_memory_manager")
|
||||||
|
)
|
||||||
|
|
||||||
# check if we are changing the active transformer, if so, we need to swap the one in
|
# check if we are changing the active transformer, if so, we need to swap the one in
|
||||||
# vram if low_vram is enabled
|
# vram if low_vram is enabled
|
||||||
# todo swap the loras as well
|
# todo swap the loras as well
|
||||||
if t_name != self._active_transformer_name:
|
if t_name != self._active_transformer_name:
|
||||||
if self.low_vram:
|
if self.low_vram and not managed:
|
||||||
getattr(self, self._active_transformer_name).to("cpu")
|
getattr(self, self._active_transformer_name).to("cpu")
|
||||||
getattr(self, t_name).to(self.device_torch)
|
getattr(self, t_name).to(self.device_torch)
|
||||||
torch.cuda.empty_cache()
|
torch.cuda.empty_cache()
|
||||||
self._active_transformer_name = t_name
|
self._active_transformer_name = t_name
|
||||||
|
|
||||||
if self.transformer.device != hidden_states.device:
|
if not managed and self.transformer.device != hidden_states.device:
|
||||||
if self.low_vram:
|
if self.low_vram:
|
||||||
# move other transformer to cpu
|
# move other transformer to cpu
|
||||||
other_tname = (
|
other_tname = (
|
||||||
|
|||||||
@@ -149,8 +149,10 @@ class ZetaChromaModel(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
# just to make sure everything is on the right device and dtype
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
text_encoder[0].to(self.device_torch)
|
# to the gpu on demand
|
||||||
|
if not self.low_vram:
|
||||||
|
text_encoder[0].to(self.device_torch)
|
||||||
text_encoder[0].requires_grad_(False)
|
text_encoder[0].requires_grad_(False)
|
||||||
text_encoder[0].eval()
|
text_encoder[0].eval()
|
||||||
flush()
|
flush()
|
||||||
|
|||||||
@@ -338,6 +338,71 @@ def run_one(arch: str, device: str, allow_download: bool) -> dict:
|
|||||||
gen_profile.disable()
|
gen_profile.disable()
|
||||||
gen_profile.dump_stats(os.path.join(out_dir, "gen.prof"))
|
gen_profile.dump_stats(os.path.join(out_dir, "gen.prof"))
|
||||||
|
|
||||||
|
# ---- 100% offload forward: attach full layer offloading to the DiT and
|
||||||
|
# text encoder(s), then run a 1-step sample so the offload path (TE
|
||||||
|
# encode + denoise + decode with every layer staged from cpu) is proven
|
||||||
|
# for every arch ----
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from toolkit.memory_management import MemoryManager
|
||||||
|
|
||||||
|
prof.stage("offload 100%: attach")
|
||||||
|
offload_targets = []
|
||||||
|
model_module = getattr(sd, "model", None)
|
||||||
|
if model_module is not None:
|
||||||
|
subs = [
|
||||||
|
getattr(model_module, name, None)
|
||||||
|
for name in ("transformer_1", "transformer_2")
|
||||||
|
]
|
||||||
|
if all(s is not None for s in subs):
|
||||||
|
# dual-DiT wrappers (wan22 14b): pipelines hold and .to() the
|
||||||
|
# sub-transformers directly, so each needs its own manager
|
||||||
|
offload_targets.extend(subs)
|
||||||
|
else:
|
||||||
|
offload_targets.append(model_module)
|
||||||
|
tes = getattr(sd, "text_encoder", None)
|
||||||
|
for te in tes if isinstance(tes, list) else [tes]:
|
||||||
|
offload_targets.append(te)
|
||||||
|
# auxiliary conditioning stacks where archs have them (ltx connectors,
|
||||||
|
# i2v vision towers)
|
||||||
|
offload_targets.append(getattr(sd, "image_encoder", None))
|
||||||
|
offload_targets.append(getattr(getattr(sd, "pipeline", None), "connectors", None))
|
||||||
|
attached = 0
|
||||||
|
for m in offload_targets:
|
||||||
|
if not isinstance(m, torch.nn.Module):
|
||||||
|
continue
|
||||||
|
if type(m).__name__.startswith("Fake"):
|
||||||
|
continue
|
||||||
|
if next(m.parameters(), None) is None:
|
||||||
|
continue
|
||||||
|
get_ignore = getattr(m, "get_offload_ignore_modules", None)
|
||||||
|
ignore = get_ignore() if callable(get_ignore) else None
|
||||||
|
MemoryManager.attach(
|
||||||
|
m,
|
||||||
|
torch.device(device),
|
||||||
|
offload_percent=1.0,
|
||||||
|
ignore_modules=list(ignore or []),
|
||||||
|
)
|
||||||
|
attached += 1
|
||||||
|
|
||||||
|
prof.stage("offload 100%: generate")
|
||||||
|
offload_profile = cProfile.Profile()
|
||||||
|
offload_profile.enable()
|
||||||
|
t_off0 = time.perf_counter()
|
||||||
|
# 2 steps, not 1: diffusers' shift_terminal stretch divides by
|
||||||
|
# one_minus_z[-1], which is zero for a single step — NaN timesteps and a
|
||||||
|
# NaN image would mask real numerical breakage in the offload path
|
||||||
|
gen_offload = GenerateImageConfig(
|
||||||
|
prompt="a photo of a cat sitting on a wooden table",
|
||||||
|
output_folder=out_dir,
|
||||||
|
output_ext="png",
|
||||||
|
**{**sample_kwargs, "num_inference_steps": 2},
|
||||||
|
)
|
||||||
|
sd.generate_images([gen_offload], **gen_kwargs)
|
||||||
|
offload_seconds = time.perf_counter() - t_off0
|
||||||
|
offload_profile.disable()
|
||||||
|
offload_profile.dump_stats(os.path.join(out_dir, "offload.prof"))
|
||||||
|
|
||||||
stages = prof.finish()
|
stages = prof.finish()
|
||||||
|
|
||||||
produced = [
|
produced = [
|
||||||
@@ -350,8 +415,6 @@ def run_one(arch: str, device: str, allow_download: bool) -> dict:
|
|||||||
if not produced:
|
if not produced:
|
||||||
raise RuntimeError(f"no output file produced in {out_dir}")
|
raise RuntimeError(f"no output file produced in {out_dir}")
|
||||||
|
|
||||||
import torch
|
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"arch": arch,
|
"arch": arch,
|
||||||
"status": "PASS",
|
"status": "PASS",
|
||||||
@@ -360,12 +423,16 @@ def run_one(arch: str, device: str, allow_download: bool) -> dict:
|
|||||||
"qtype_te": model_config.qtype_te if model_kwargs.get("quantize_te") else None,
|
"qtype_te": model_config.qtype_te if model_kwargs.get("quantize_te") else None,
|
||||||
"load_seconds": round(load_seconds, 3),
|
"load_seconds": round(load_seconds, 3),
|
||||||
"gen_seconds": round(gen_seconds, 3),
|
"gen_seconds": round(gen_seconds, 3),
|
||||||
|
"offload_seconds": round(offload_seconds, 3),
|
||||||
|
"offload_modules_attached": attached,
|
||||||
"stages": stages,
|
"stages": stages,
|
||||||
"profile": {
|
"profile": {
|
||||||
"load_top": profile_top(load_profile),
|
"load_top": profile_top(load_profile),
|
||||||
"gen_top": profile_top(gen_profile, limit=15),
|
"gen_top": profile_top(gen_profile, limit=15),
|
||||||
|
"offload_top": profile_top(offload_profile, limit=15),
|
||||||
"load_prof": os.path.join(out_dir, "load.prof"),
|
"load_prof": os.path.join(out_dir, "load.prof"),
|
||||||
"gen_prof": os.path.join(out_dir, "gen.prof"),
|
"gen_prof": os.path.join(out_dir, "gen.prof"),
|
||||||
|
"offload_prof": os.path.join(out_dir, "offload.prof"),
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"torch_num_threads": torch.get_num_threads(),
|
"torch_num_threads": torch.get_num_threads(),
|
||||||
@@ -577,7 +644,8 @@ def main():
|
|||||||
total = len(MODEL_TESTS)
|
total = len(MODEL_TESTS)
|
||||||
sweep_t0 = time.perf_counter()
|
sweep_t0 = time.perf_counter()
|
||||||
for i, arch in enumerate(MODEL_TESTS, start=1):
|
for i, arch in enumerate(MODEL_TESTS, start=1):
|
||||||
print(f"\n===== {arch} ({i}/{total}) =====")
|
# flush so tail -f shows headers/progress live when stdout is a file
|
||||||
|
print(f"\n===== {arch} ({i}/{total}) =====", flush=True)
|
||||||
result_path = os.path.join(OUTPUT_ROOT, f".{arch.replace('/', '_')}.result.json")
|
result_path = os.path.join(OUTPUT_ROOT, f".{arch.replace('/', '_')}.result.json")
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
@@ -610,7 +678,8 @@ def main():
|
|||||||
print(
|
print(
|
||||||
f">>> progress: {i}/{total} ({i * 100 // total}%) — "
|
f">>> progress: {i}/{total} ({i * 100 // total}%) — "
|
||||||
f"{tally['PASS']} pass, {tally['FAIL']} fail, {tally['SKIP']} skip — "
|
f"{tally['PASS']} pass, {tally['FAIL']} fail, {tally['SKIP']} skip — "
|
||||||
f"elapsed {elapsed / 60:.0f}m, eta ~{eta / 60:.0f}m"
|
f"elapsed {elapsed / 60:.0f}m, eta ~{eta / 60:.0f}m",
|
||||||
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
print("\n===== summary =====")
|
print("\n===== summary =====")
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ def hacked_state_dict(self, *args, **kwargs):
|
|||||||
|
|
||||||
# hacks the state dict so we can dequantize before saving
|
# hacks the state dict so we can dequantize before saving
|
||||||
def patch_dequantization_on_save(model):
|
def patch_dequantization_on_save(model):
|
||||||
|
# idempotent: re-patching would capture the hacked state_dict as
|
||||||
|
# orig_state_dict and recurse on save
|
||||||
|
if getattr(model, "orig_state_dict", None) is not None:
|
||||||
|
return
|
||||||
model.orig_state_dict = model.state_dict
|
model.orig_state_dict = model.state_dict
|
||||||
model.state_dict = partial(hacked_state_dict, model)
|
model.state_dict = partial(hacked_state_dict, model)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .manager_modules import (
|
|||||||
LinearLayerMemoryManager,
|
LinearLayerMemoryManager,
|
||||||
ConvLayerMemoryManager,
|
ConvLayerMemoryManager,
|
||||||
OstrisLinearLayerMemoryManager,
|
OstrisLinearLayerMemoryManager,
|
||||||
|
EmbeddingLayerMemoryManager,
|
||||||
_DEVICE_STATE,
|
_DEVICE_STATE,
|
||||||
)
|
)
|
||||||
import random
|
import random
|
||||||
@@ -92,12 +93,59 @@ class MemoryManager:
|
|||||||
module._mm_to = module.to
|
module._mm_to = module.to
|
||||||
module.to = module._memory_manager.memory_managed_to
|
module.to = module._memory_manager.memory_managed_to
|
||||||
|
|
||||||
# add ignore modules to unmanaged list
|
# a fully offloaded module's parameters all live on cpu, which makes
|
||||||
|
# ModelMixin.device (and pipelines deriving their execution device
|
||||||
|
# from it) report "cpu" and plant latents/timesteps there. Report the
|
||||||
|
# compute device instead via an in-place subclass (same class-swap
|
||||||
|
# pattern as OstrisLinear/adopt_component); detach() restores it.
|
||||||
|
try:
|
||||||
|
module._mm_orig_class = module.__class__
|
||||||
|
managed_cls = type(
|
||||||
|
module.__class__.__name__,
|
||||||
|
(module.__class__,),
|
||||||
|
{
|
||||||
|
"device": property(
|
||||||
|
lambda self: self._memory_manager.process_device,
|
||||||
|
lambda self, value: self.__dict__.__setitem__(
|
||||||
|
"_mm_device_shadow", value
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# transformers keys per-class registries (e.g. the hidden-states
|
||||||
|
# capture specs) by str(model.__class__); make the subclass
|
||||||
|
# stringify identically so those lookups still hit
|
||||||
|
managed_cls.__module__ = module.__class__.__module__
|
||||||
|
managed_cls.__qualname__ = module.__class__.__qualname__
|
||||||
|
module.__class__ = managed_cls
|
||||||
|
except TypeError:
|
||||||
|
# exotic class layouts (__slots__ etc.): keep the original class
|
||||||
|
module._mm_orig_class = None
|
||||||
|
|
||||||
|
# add ignore modules to unmanaged list; they must stay RESIDENT on the
|
||||||
|
# compute device (fp32 tables, pad tokens) — a model attached while
|
||||||
|
# parked on cpu would otherwise feed cpu tensors into gpu math
|
||||||
for im in ignore_modules:
|
for im in ignore_modules:
|
||||||
module._memory_manager.unmanaged_modules.append(im)
|
module._memory_manager.unmanaged_modules.append(im)
|
||||||
|
try:
|
||||||
|
if isinstance(im, torch.Tensor):
|
||||||
|
im.data = im.data.to(device)
|
||||||
|
elif isinstance(im, torch.nn.Module):
|
||||||
|
im.to(device)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# count ignore modules as processed
|
# count ignore modules as processed
|
||||||
modules_processed = [x for x in ignore_modules]
|
modules_processed = [x for x in ignore_modules]
|
||||||
|
|
||||||
|
# weights tied to an embedding (lm_head <-> embed_tokens) must not be
|
||||||
|
# managed: pinning the linear's weight to cpu strands the (unmanaged)
|
||||||
|
# embedding that shares the same tensor
|
||||||
|
embedding_weight_ptrs = {
|
||||||
|
m.weight.data_ptr()
|
||||||
|
for m in module.modules()
|
||||||
|
if isinstance(m, torch.nn.Embedding)
|
||||||
|
}
|
||||||
# attach to all modules
|
# attach to all modules
|
||||||
for name, sub_module in module.named_modules():
|
for name, sub_module in module.named_modules():
|
||||||
for child_name, child_module in sub_module.named_modules():
|
for child_name, child_module in sub_module.named_modules():
|
||||||
@@ -110,6 +158,12 @@ class MemoryManager:
|
|||||||
# randomly skip some modules
|
# randomly skip some modules
|
||||||
if random.random() > offload_percent:
|
if random.random() > offload_percent:
|
||||||
skip = True
|
skip = True
|
||||||
|
if (
|
||||||
|
not getattr(child_module, "is_ostris_quantized", False)
|
||||||
|
and isinstance(getattr(child_module, "weight", None), torch.Tensor)
|
||||||
|
and child_module.weight.data_ptr() in embedding_weight_ptrs
|
||||||
|
):
|
||||||
|
skip = True
|
||||||
if skip:
|
if skip:
|
||||||
module._memory_manager.unmanaged_modules.append(child_module)
|
module._memory_manager.unmanaged_modules.append(child_module)
|
||||||
else:
|
else:
|
||||||
@@ -158,12 +212,40 @@ class MemoryManager:
|
|||||||
)
|
)
|
||||||
modules_processed.append(ara)
|
modules_processed.append(ara)
|
||||||
modules_processed.append(child_module)
|
modules_processed.append(child_module)
|
||||||
|
elif (
|
||||||
|
isinstance(child_module, torch.nn.Embedding)
|
||||||
|
and child_module not in modules_processed
|
||||||
|
and child_module.weight.numel()
|
||||||
|
* child_module.weight.element_size()
|
||||||
|
> 64 * 1024 * 1024
|
||||||
|
):
|
||||||
|
# (the cpu gather is autograd-transparent, so a trainable
|
||||||
|
# embedding still gets grads — they just land on cpu)
|
||||||
|
# large frozen vocab table: cpu-resident, rows gathered on
|
||||||
|
# cpu (only the looked-up tokens cross the bus)
|
||||||
|
EmbeddingLayerMemoryManager.attach(
|
||||||
|
child_module, module._memory_manager
|
||||||
|
)
|
||||||
|
# the cpu move replaced weight.data, so a tied lm_head no
|
||||||
|
# longer matches the (original-ptr) tied guard and gets
|
||||||
|
# managed normally — correct: the shared weight now lives
|
||||||
|
# on cpu, so the linear must bounce it
|
||||||
|
modules_processed.append(child_module)
|
||||||
elif child_module.__class__.__name__ in UNMANAGED_MODULES or any(
|
elif child_module.__class__.__name__ in UNMANAGED_MODULES or any(
|
||||||
inc in child_module.__class__.__name__
|
inc in child_module.__class__.__name__
|
||||||
for inc in UNMANAGED_MODULES_INCLUDES
|
for inc in UNMANAGED_MODULES_INCLUDES
|
||||||
):
|
):
|
||||||
# unmanaged
|
# unmanaged — but never re-list a module the nested walk
|
||||||
module._memory_manager.unmanaged_modules.append(child_module)
|
# already managed (a managed Embedding landing here would
|
||||||
|
# get hauled back to the gpu by memory_managed_to), and
|
||||||
|
# don't append duplicates
|
||||||
|
if (
|
||||||
|
child_module not in modules_processed
|
||||||
|
and not hasattr(child_module, "_layer_memory_manager")
|
||||||
|
and child_module
|
||||||
|
not in module._memory_manager.unmanaged_modules
|
||||||
|
):
|
||||||
|
module._memory_manager.unmanaged_modules.append(child_module)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -179,6 +261,10 @@ class MemoryManager:
|
|||||||
if not hasattr(module, "_memory_manager"):
|
if not hasattr(module, "_memory_manager"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if getattr(module, "_mm_orig_class", None) is not None:
|
||||||
|
module.__class__ = module._mm_orig_class
|
||||||
|
del module._mm_orig_class
|
||||||
|
|
||||||
for unmanaged in module._memory_manager.unmanaged_modules:
|
for unmanaged in module._memory_manager.unmanaged_modules:
|
||||||
try:
|
try:
|
||||||
if isinstance(unmanaged, torch.Tensor):
|
if isinstance(unmanaged, torch.Tensor):
|
||||||
|
|||||||
@@ -312,6 +312,12 @@ class _BouncingLinearFn(torch.autograd.Function):
|
|||||||
ctx.device = torch.device("cpu")
|
ctx.device = torch.device("cpu")
|
||||||
return out.to(x.device)
|
return out.to(x.device)
|
||||||
|
|
||||||
|
if x.device != device:
|
||||||
|
# a pipeline that derives its execution device from the module's
|
||||||
|
# (fully offloaded -> "cpu") parameters can feed cpu activations;
|
||||||
|
# compute happens on the staged device and the output stays there
|
||||||
|
x = x.to(device, non_blocking=True)
|
||||||
|
|
||||||
state = _get_device_state(device)
|
state = _get_device_state(device)
|
||||||
# the guard makes current_stream() (used by the staging helpers' event
|
# the guard makes current_stream() (used by the staging helpers' event
|
||||||
# waits/records) resolve to the process device; without it they hit
|
# waits/records) resolve to the process device; without it they hit
|
||||||
@@ -461,6 +467,11 @@ class _BouncingConv2dFn(torch.autograd.Function):
|
|||||||
ctx.meta = ("cpu", stride, padding, dilation, groups, target_dtype)
|
ctx.meta = ("cpu", stride, padding, dilation, groups, target_dtype)
|
||||||
return out.to(x.device)
|
return out.to(x.device)
|
||||||
|
|
||||||
|
if x.device != device:
|
||||||
|
# cpu activations from a fully-offloaded pipeline: see
|
||||||
|
# _BouncingLinearFn.forward
|
||||||
|
x = x.to(device, non_blocking=True)
|
||||||
|
|
||||||
state = _get_device_state(device)
|
state = _get_device_state(device)
|
||||||
# device guard: see _BouncingLinearFn.forward
|
# device guard: see _BouncingLinearFn.forward
|
||||||
with torch.cuda.device(device):
|
with torch.cuda.device(device):
|
||||||
@@ -631,6 +642,42 @@ class BaseLayerMemoryManager:
|
|||||||
param._is_memory_managed = True
|
param._is_memory_managed = True
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddingLayerMemoryManager(BaseLayerMemoryManager):
|
||||||
|
"""Offloads a (large, frozen) nn.Embedding by keeping the weight on cpu
|
||||||
|
and doing the row gather THERE: instead of staging a multi-GB vocab table
|
||||||
|
to the gpu, only the looked-up rows (tokens x dim, ~KBs) cross the bus.
|
||||||
|
Lookups happen once per prompt, so the cpu gather is free."""
|
||||||
|
|
||||||
|
def __init__(self, module: nn.Module, manager: "MemoryManager"):
|
||||||
|
super().__init__(module, manager)
|
||||||
|
|
||||||
|
# cpu-resident weight; no pinning — the weight never crosses the bus
|
||||||
|
module.weight.data = module.weight.data.to("cpu")
|
||||||
|
|
||||||
|
self._original_forward = module.forward
|
||||||
|
|
||||||
|
def _mm_forward(input_ids, *args, **kwargs):
|
||||||
|
if args or kwargs:
|
||||||
|
return self._original_forward(input_ids, *args, **kwargs)
|
||||||
|
out_device = (
|
||||||
|
input_ids.device
|
||||||
|
if input_ids.device.type == "cuda"
|
||||||
|
else self.manager.process_device
|
||||||
|
)
|
||||||
|
out = F.embedding(
|
||||||
|
input_ids.to("cpu"),
|
||||||
|
self.module.weight,
|
||||||
|
self.module.padding_idx,
|
||||||
|
self.module.max_norm,
|
||||||
|
self.module.norm_type,
|
||||||
|
self.module.scale_grad_by_freq,
|
||||||
|
self.module.sparse,
|
||||||
|
)
|
||||||
|
return out.to(out_device, non_blocking=True)
|
||||||
|
|
||||||
|
module.forward = _mm_forward
|
||||||
|
|
||||||
|
|
||||||
class LinearLayerMemoryManager(BaseLayerMemoryManager):
|
class LinearLayerMemoryManager(BaseLayerMemoryManager):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -739,6 +786,11 @@ class OstrisLinearLayerMemoryManager(BaseLayerMemoryManager):
|
|||||||
# already resident on device
|
# already resident on device
|
||||||
return self._original_forward(x)
|
return self._original_forward(x)
|
||||||
|
|
||||||
|
if x.device != device:
|
||||||
|
# cpu activations from a fully-offloaded pipeline: see
|
||||||
|
# _BouncingLinearFn.forward
|
||||||
|
x = x.to(device, non_blocking=True)
|
||||||
|
|
||||||
state = _get_device_state(device)
|
state = _get_device_state(device)
|
||||||
d = state["depth"]
|
d = state["depth"]
|
||||||
idx = state["forward_clk"]
|
idx = state["forward_clk"]
|
||||||
|
|||||||
@@ -470,11 +470,75 @@ metrics_baseline.json). Profiling findings and fixes:
|
|||||||
- posix_fadvise(WILLNEED) readahead on single-file loads (mixin._readahead)
|
- posix_fadvise(WILLNEED) readahead on single-file loads (mixin._readahead)
|
||||||
— ~10-15% on cold reads, free when warm, no-op off POSIX.
|
— ~10-15% on cold reads, free when warm, no-op off POSIX.
|
||||||
|
|
||||||
|
Round 2 (same day):
|
||||||
|
- ARA path (`attach_ara_and_quantize`) now quantizes each hijacked linear on
|
||||||
|
the gpu (was: wherever it lived, historically the cpu) with the same
|
||||||
|
keep_on_device policy; the uint8 extras pass follows suit. Verified with
|
||||||
|
hidream + its uint3 ARA end to end.
|
||||||
|
- `quantize_model` delegates its non-ARA branch to `quantize_module`
|
||||||
|
(keep_on_device derived from model_config); dead `_quantize_model_blocks`
|
||||||
|
removed; `patch_dequantization_on_save` made idempotent (double-patching
|
||||||
|
would have recursed on save).
|
||||||
|
- Local-first hub loads: `_local_first` wraps from_pretrained / load_config /
|
||||||
|
AutoTokenizer / AutoProcessor — fully-cached repos load with
|
||||||
|
local_files_only (13x on VAE metadata, 0.53s -> 0.04s; a failed local
|
||||||
|
attempt is instant, so uncached repos fall through to the online path at
|
||||||
|
zero cost). Tokenizer-load slowness was DIAGNOSED as these per-process hub
|
||||||
|
etag HEADs, not the use_fast=False sites.
|
||||||
|
|
||||||
|
Round 3 — vram spike elimination (2026-08-28):
|
||||||
|
- keep_on_device's extras pass had moved the whole unquantized remainder to
|
||||||
|
the gpu in bf16 before quantizing (+7-10GB transient over final residency
|
||||||
|
on big-extras models). Now `quantize(keep_on_quantize_device=True)`
|
||||||
|
quantizes each extra layer on the gpu and leaves it — transient is one
|
||||||
|
layer; non-quantized leftovers move only at final placement. Same for the
|
||||||
|
ARA uint8 pass.
|
||||||
|
- convrot8 `quantize_` chunked: takes the weight at stored dtype
|
||||||
|
(wants_fp32_weight=False; ConvRotIntN keeps True) and rotates+quantizes in
|
||||||
|
256MB row-chunks — a 152k-vocab TE projection's transient fell 7.6GB ->
|
||||||
|
1.4GB. Sub-threshold layers keep the one-shot path, verified bit-identical.
|
||||||
|
- qwen_image measured: standard load peak 35.3 -> 27.5GB (below the 28.0
|
||||||
|
baseline; process peak now set by generation); low_vram load quantizes the
|
||||||
|
whole 20B stack never holding >2.4GB gpu.
|
||||||
|
|
||||||
Ideas not yet done: pinned-buffer staged h2d (~+60% warm-upload bandwidth,
|
Ideas not yet done: pinned-buffer staged h2d (~+60% warm-upload bandwidth,
|
||||||
9.5 vs 5.8GB/s measured), overlapping h2d with quantize kernels (small — the
|
9.5 vs 5.8GB/s measured), overlapping h2d with quantize kernels (small — the
|
||||||
kernels are ~2% of the pass), wan22 low_vram generate choreography (165s of
|
kernels are ~2% of the pass), wan22 low_vram generate choreography (165s of
|
||||||
unload/reload per sample), extending keep_on_device to the legacy
|
unload/reload per sample), holders' legacy "just to make sure" .to(device)
|
||||||
quantize_model/trainer path.
|
lines that move quantized TEs to gpu even under low_vram (qwen's Preparing
|
||||||
|
Model stage parks 7.6GB — policy question, not a quantize spike).
|
||||||
|
|
||||||
|
## Offload hardening + final certification (2026-08-28)
|
||||||
|
|
||||||
|
The harness gained a per-arch 100%-offload probe (attach MemoryManager at
|
||||||
|
offload_percent=1.0 to the DiT + TEs + connectors/vision towers, then a
|
||||||
|
2-step sample — 2 not 1: diffusers' shift_terminal stretch divides by zero
|
||||||
|
at a single step). It flushed out and fixed, in the manager:
|
||||||
|
- fully-offloaded modules now REPORT the compute device (class swap with
|
||||||
|
stringification-stable __module__/__qualname__ — transformers keys its
|
||||||
|
hidden-states capture registry by str(class)); bouncing ops also pull cpu
|
||||||
|
activations to the staged device
|
||||||
|
- weights tied to embeddings (lm_head <-> embed_tokens) are never pinned out
|
||||||
|
from under the unmanaged embedding
|
||||||
|
- ignore_modules are moved to the compute device at attach
|
||||||
|
- the nested attach walk no longer double-lists managed modules as unmanaged
|
||||||
|
(a managed Embedding was getting hauled back to gpu by pipeline.to)
|
||||||
|
- NEW: bouncing embeddings — vocab tables >64MB stay cpu-resident with the
|
||||||
|
row gather done ON cpu (only looked-up rows cross the bus); tied lm_heads
|
||||||
|
fall through to normal bouncing
|
||||||
|
- wan22's dual wrapper skips its whole-14B .to() swaps when memory-managed,
|
||||||
|
and offload attaches per sub-transformer (pipelines hold those directly)
|
||||||
|
|
||||||
|
Measured offload-phase peaks: wan22_14b 14.7 -> 1.5GB, ltx2.5 14.0 ->
|
||||||
|
11.8GB (remainder: video activations + resident VAEs/vocoder by design),
|
||||||
|
most image archs 1-4GB vs 10-53GB resident. flux2's Mistral warnings
|
||||||
|
silenced behavior-preservingly (tie_word_embeddings=False, explicit
|
||||||
|
fix_mistral_regex=False, processor_kwargs — tokenization verified
|
||||||
|
bit-identical). ConvRot skip notices gated to layers >=1M weights.
|
||||||
|
|
||||||
|
FINAL CERTIFICATION: full sweep, 33/34 PASS, 0 FAIL, 1 SKIP (mageflow
|
||||||
|
upstream 404) — load + convrot8 quantize + generate + 100%-offload forward
|
||||||
|
for every arch on the complete optimized stack.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
+47
-22
@@ -139,13 +139,29 @@ class OstrisModelMixin:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
|
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
|
||||||
return cls.from_pretrained(
|
return cls._local_first(
|
||||||
path, subfolder=subfolder, torch_dtype=dtype, **kwargs
|
cls.from_pretrained, path, subfolder=subfolder, torch_dtype=dtype, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_load_config(cls, path, subfolder=None):
|
def aitk_load_config(cls, path, subfolder=None):
|
||||||
return cls.load_config(path, subfolder=subfolder)
|
return cls._local_first(cls.load_config, path, subfolder=subfolder)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _local_first(loader, *args, **kwargs):
|
||||||
|
"""Serve fully-cached repos without touching the hub. Online
|
||||||
|
from_pretrained does an etag HEAD round-trip per file (seconds of pure
|
||||||
|
network latency on every warm load); a local_files_only attempt is
|
||||||
|
instant when the cache can't satisfy it, so failing over to the
|
||||||
|
online path costs nothing."""
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
if _os.environ.get("HF_HUB_OFFLINE") == "1":
|
||||||
|
return loader(*args, **kwargs)
|
||||||
|
try:
|
||||||
|
return loader(*args, local_files_only=True, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
return loader(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_from_config(cls, config):
|
def aitk_from_config(cls, config):
|
||||||
@@ -247,6 +263,17 @@ class OstrisModelMixin:
|
|||||||
exclude = list(exclude_quant_modules or []) + list(
|
exclude = list(exclude_quant_modules or []) + list(
|
||||||
self.get_quantization_exclude_modules() or []
|
self.get_quantization_exclude_modules() or []
|
||||||
)
|
)
|
||||||
|
q_device = quantize_device if quantize_device is not None else device
|
||||||
|
# final home is the quantize gpu (no offloading, no cpu
|
||||||
|
# parking): quantized weights stay put — one bus crossing
|
||||||
|
# instead of three, and no model-sized host-ram copy
|
||||||
|
keep_on_device = (
|
||||||
|
not offload
|
||||||
|
and device is not None
|
||||||
|
and q_device is not None
|
||||||
|
and torch.device(device) == torch.device(q_device)
|
||||||
|
and torch.device(device).type != "cpu"
|
||||||
|
)
|
||||||
if ara_path is not None:
|
if ara_path is not None:
|
||||||
if base_model is None:
|
if base_model is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -254,20 +281,16 @@ class OstrisModelMixin:
|
|||||||
"network wiring"
|
"network wiring"
|
||||||
)
|
)
|
||||||
status_fn("Quantizing with accuracy recovery adapter")
|
status_fn("Quantizing with accuracy recovery adapter")
|
||||||
attach_ara_and_quantize(base_model, self, ara_path, exclude=exclude)
|
attach_ara_and_quantize(
|
||||||
|
base_model,
|
||||||
|
self,
|
||||||
|
ara_path,
|
||||||
|
exclude=exclude,
|
||||||
|
device=q_device,
|
||||||
|
keep_on_device=keep_on_device,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
status_fn(f"Quantizing ({qtype})")
|
status_fn(f"Quantizing ({qtype})")
|
||||||
q_device = quantize_device if quantize_device is not None else device
|
|
||||||
# final home is the quantize gpu (no offloading, no cpu
|
|
||||||
# parking): quantized weights stay put — one bus crossing
|
|
||||||
# instead of three, and no model-sized host-ram copy
|
|
||||||
keep_on_device = (
|
|
||||||
not offload
|
|
||||||
and device is not None
|
|
||||||
and q_device is not None
|
|
||||||
and torch.device(device) == torch.device(q_device)
|
|
||||||
and torch.device(device).type != "cpu"
|
|
||||||
)
|
|
||||||
quantize_module(
|
quantize_module(
|
||||||
self,
|
self,
|
||||||
qtype,
|
qtype,
|
||||||
@@ -623,8 +646,8 @@ class OstrisModelMixin:
|
|||||||
os.path.join(source, subfolder)
|
os.path.join(source, subfolder)
|
||||||
):
|
):
|
||||||
subfolder = None
|
subfolder = None
|
||||||
return AutoTokenizer.from_pretrained(
|
return cls._local_first(
|
||||||
source, subfolder=subfolder or "", **kwargs
|
AutoTokenizer.from_pretrained, source, subfolder=subfolder or "", **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -645,8 +668,8 @@ class OstrisModelMixin:
|
|||||||
os.path.join(source, subfolder)
|
os.path.join(source, subfolder)
|
||||||
):
|
):
|
||||||
subfolder = None
|
subfolder = None
|
||||||
return AutoProcessor.from_pretrained(
|
return cls._local_first(
|
||||||
source, subfolder=subfolder or "", **kwargs
|
AutoProcessor.from_pretrained, source, subfolder=subfolder or "", **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -778,15 +801,17 @@ class OstrisTransformersMixin(OstrisModelMixin):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
|
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
|
||||||
return cls.from_pretrained(
|
return cls._local_first(
|
||||||
path, subfolder=subfolder or "", torch_dtype=dtype, **kwargs
|
cls.from_pretrained, path, subfolder=subfolder or "", torch_dtype=dtype, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_load_config(cls, path, subfolder=None):
|
def aitk_load_config(cls, path, subfolder=None):
|
||||||
from transformers import AutoConfig
|
from transformers import AutoConfig
|
||||||
|
|
||||||
return AutoConfig.from_pretrained(path, subfolder=subfolder or "")
|
return cls._local_first(
|
||||||
|
AutoConfig.from_pretrained, path, subfolder=subfolder or ""
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def aitk_from_config(cls, config):
|
def aitk_from_config(cls, config):
|
||||||
|
|||||||
@@ -433,7 +433,10 @@ class Wan21(BaseModel):
|
|||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
text_encoder.to(self.device_torch)
|
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||||
|
# to the gpu on demand
|
||||||
|
if not self.model_config.low_vram:
|
||||||
|
text_encoder.to(self.device_torch)
|
||||||
text_encoder.requires_grad_(False)
|
text_encoder.requires_grad_(False)
|
||||||
text_encoder.eval()
|
text_encoder.eval()
|
||||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||||
|
|||||||
@@ -662,7 +662,7 @@ class ConvRotQuantizer(OstrisQuantizer):
|
|||||||
d = module.in_features
|
d = module.in_features
|
||||||
rot = self._rot_for(d)
|
rot = self._rot_for(d)
|
||||||
if d % BLOCK != 0 or module.out_features % BLOCK != 0 or rot < 16:
|
if d % BLOCK != 0 or module.out_features % BLOCK != 0 or rot < 16:
|
||||||
if d not in _skip_warned:
|
if d not in _skip_warned and d * module.out_features >= 1 << 20:
|
||||||
_skip_warned.add(d)
|
_skip_warned.add(d)
|
||||||
print_acc(
|
print_acc(
|
||||||
f"ConvRot: skipping linears with in_features={d} "
|
f"ConvRot: skipping linears with in_features={d} "
|
||||||
@@ -1312,7 +1312,7 @@ class ConvRotInt8Quantizer(OstrisQuantizer):
|
|||||||
def can_quantize(self, module: torch.nn.Linear) -> bool:
|
def can_quantize(self, module: torch.nn.Linear) -> bool:
|
||||||
d = module.in_features
|
d = module.in_features
|
||||||
if d % BLOCK != 0 or module.out_features % 8 != 0 or self._rot_for(d) < 16:
|
if d % BLOCK != 0 or module.out_features % 8 != 0 or self._rot_for(d) < 16:
|
||||||
if d not in _skip_warned:
|
if d not in _skip_warned and d * module.out_features >= 1 << 20:
|
||||||
_skip_warned.add(d)
|
_skip_warned.add(d)
|
||||||
print_acc(
|
print_acc(
|
||||||
f"ConvRot: skipping linears with in_features={d} "
|
f"ConvRot: skipping linears with in_features={d} "
|
||||||
@@ -1324,7 +1324,7 @@ class ConvRotInt8Quantizer(OstrisQuantizer):
|
|||||||
# (torch._int_mm raises CUBLAS_STATUS_NOT_SUPPORTED), e.g.
|
# (torch._int_mm raises CUBLAS_STATUS_NOT_SUPPORTED), e.g.
|
||||||
# omnigen2's 64 -> 2520 x_embedder
|
# omnigen2's 64 -> 2520 x_embedder
|
||||||
key = (d, module.out_features)
|
key = (d, module.out_features)
|
||||||
if key not in _skip_warned:
|
if key not in _skip_warned and d * module.out_features >= 1 << 20:
|
||||||
_skip_warned.add(key)
|
_skip_warned.add(key)
|
||||||
print_acc(
|
print_acc(
|
||||||
f"ConvRot: skipping linears with in_features={d}, "
|
f"ConvRot: skipping linears with in_features={d}, "
|
||||||
@@ -1334,9 +1334,32 @@ class ConvRotInt8Quantizer(OstrisQuantizer):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# takes the weight in its stored dtype and casts per row-chunk below: a
|
||||||
|
# full-size fp32 copy of a huge layer (e.g. a TE's vocab projection) was
|
||||||
|
# a multi-GB vram transient on top of the full-size rotated fp32 tensor
|
||||||
|
wants_fp32_weight = False
|
||||||
|
|
||||||
|
# fp32 working-set target per chunk; layers under this keep the one-shot
|
||||||
|
# path (identical math and bit-identical output to the historical code)
|
||||||
|
_QUANT_CHUNK_BYTES = 256 * 1024 * 1024
|
||||||
|
|
||||||
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
|
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
|
||||||
rot = self._rot_for(module.in_features)
|
rot = self._rot_for(module.in_features)
|
||||||
q, scales = quantize_int8_rows(rotate(weight_fp32, rot))
|
rows, in_f = weight_fp32.shape
|
||||||
|
chunk_rows = max(1, self._QUANT_CHUNK_BYTES // (in_f * 4))
|
||||||
|
if rows <= chunk_rows:
|
||||||
|
q, scales = quantize_int8_rows(rotate(weight_fp32.float(), rot))
|
||||||
|
else:
|
||||||
|
# row-chunked: rotation and per-row scaling are row-independent,
|
||||||
|
# so the fp32 transient stays ~3 chunks instead of ~4x the layer
|
||||||
|
q = torch.empty(rows, in_f, dtype=torch.int8, device=weight_fp32.device)
|
||||||
|
scales = torch.empty(rows, dtype=torch.float32, device=weight_fp32.device)
|
||||||
|
for i in range(0, rows, chunk_rows):
|
||||||
|
qc, sc = quantize_int8_rows(
|
||||||
|
rotate(weight_fp32[i : i + chunk_rows].float(), rot)
|
||||||
|
)
|
||||||
|
q[i : i + chunk_rows] = qc
|
||||||
|
scales[i : i + chunk_rows] = sc
|
||||||
module.register_buffer("cr8_qdata", q, persistent=False)
|
module.register_buffer("cr8_qdata", q, persistent=False)
|
||||||
# fp32 scales stored as a uint8 byte view (see convrot4: module.to(dtype=...)
|
# fp32 scales stored as a uint8 byte view (see convrot4: module.to(dtype=...)
|
||||||
# would otherwise cast them)
|
# would otherwise cast them)
|
||||||
@@ -2270,6 +2293,10 @@ class ConvRotIntNQuantizer(ConvRotInt8Quantizer):
|
|||||||
weight grid and bitpacked storage. One instance per bit-width, shareable
|
weight grid and bitpacked storage. One instance per bit-width, shareable
|
||||||
across modules."""
|
across modules."""
|
||||||
|
|
||||||
|
# this class's own quantize_ still expects the fp32 copy (the parent's
|
||||||
|
# chunked-cast path does not apply to it)
|
||||||
|
wants_fp32_weight = True
|
||||||
|
|
||||||
def __init__(self, bits: int, rot_size: int = 256):
|
def __init__(self, bits: int, rot_size: int = 256):
|
||||||
super().__init__(rot_size=rot_size)
|
super().__init__(rot_size=rot_size)
|
||||||
self.bits = bits
|
self.bits = bits
|
||||||
|
|||||||
+70
-74
@@ -133,6 +133,7 @@ def quantize(
|
|||||||
include: Optional[Union[str, List[str]]] = None,
|
include: Optional[Union[str, List[str]]] = None,
|
||||||
exclude: Optional[Union[str, List[str]]] = None,
|
exclude: Optional[Union[str, List[str]]] = None,
|
||||||
quantize_device: Optional[torch.device] = None,
|
quantize_device: Optional[torch.device] = None,
|
||||||
|
keep_on_quantize_device: bool = False,
|
||||||
):
|
):
|
||||||
"""Quantize the specified model submodules
|
"""Quantize the specified model submodules
|
||||||
|
|
||||||
@@ -163,6 +164,11 @@ def quantize(
|
|||||||
If provided, each module is moved to this device to quantize, then moved
|
If provided, each module is moved to this device to quantize, then moved
|
||||||
back to the device its weights were on initially. Lets a CPU-resident
|
back to the device its weights were on initially. Lets a CPU-resident
|
||||||
model (low vram) quantize layer-by-layer on the GPU.
|
model (low vram) quantize layer-by-layer on the GPU.
|
||||||
|
keep_on_quantize_device (`bool`):
|
||||||
|
With quantize_device set: leave each layer on the quantize device after
|
||||||
|
quantizing instead of moving it back. The gpu transient is then one
|
||||||
|
bf16 layer at a time — never the whole unquantized remainder at once —
|
||||||
|
for models whose final home is that device.
|
||||||
"""
|
"""
|
||||||
if include is not None:
|
if include is not None:
|
||||||
include = [include] if isinstance(include, str) else include
|
include = [include] if isinstance(include, str) else include
|
||||||
@@ -237,7 +243,7 @@ def quantize(
|
|||||||
optimizer=optimizer,
|
optimizer=optimizer,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if orig_device is not None:
|
if orig_device is not None and not keep_on_quantize_device:
|
||||||
# quanto replaces the module in its parent, so re-fetch by name
|
# quanto replaces the module in its parent, so re-fetch by name
|
||||||
model.get_submodule(name).to(orig_device)
|
model.get_submodule(name).to(orig_device)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -361,10 +367,21 @@ def quantize_module(
|
|||||||
|
|
||||||
status_fn(" - quantizing extras")
|
status_fn(" - quantizing extras")
|
||||||
if keep_on_device:
|
if keep_on_device:
|
||||||
# blocks already live on the gpu; bring the remainder over and run the
|
# blocks already live on the gpu; quantize each remaining layer there
|
||||||
# extras pass there (fast kernels instead of an all-cores cpu burst)
|
# one at a time and leave it — the transient is one bf16 layer, never
|
||||||
|
# the whole unquantized remainder at once (that transient bump could
|
||||||
|
# exceed the model's final footprint by GBs on big-extras models)
|
||||||
|
quantize(
|
||||||
|
module,
|
||||||
|
weights=quantization_type,
|
||||||
|
exclude=exclude,
|
||||||
|
quantize_device=device,
|
||||||
|
keep_on_quantize_device=True,
|
||||||
|
**quantize_kwargs,
|
||||||
|
)
|
||||||
|
# non-quantized leftovers (norms, embeddings, buffers) follow — final
|
||||||
|
# residency, not a transient
|
||||||
module.to(device)
|
module.to(device)
|
||||||
quantize(module, weights=quantization_type, exclude=exclude, **quantize_kwargs)
|
|
||||||
else:
|
else:
|
||||||
# cpu-resident model: quantize each extra layer with a gpu round-trip
|
# cpu-resident model: quantize each extra layer with a gpu round-trip
|
||||||
quantize(
|
quantize(
|
||||||
@@ -395,15 +412,35 @@ def quantize_model(
|
|||||||
# sensitive modules to keep in full precision (fnmatch patterns)
|
# sensitive modules to keep in full precision (fnmatch patterns)
|
||||||
exclude_modules = base_model.get_quantization_exclude_modules() or []
|
exclude_modules = base_model.get_quantization_exclude_modules() or []
|
||||||
|
|
||||||
if base_model.model_config.accuracy_recovery_adapter is not None:
|
mc = base_model.model_config
|
||||||
|
device = base_model.device_torch
|
||||||
|
keep_on_device = (
|
||||||
|
not mc.low_vram
|
||||||
|
and not (mc.layer_offloading and mc.layer_offloading_transformer_percent > 0)
|
||||||
|
and torch.device(device).type != "cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
if mc.accuracy_recovery_adapter is not None:
|
||||||
attach_ara_and_quantize(
|
attach_ara_and_quantize(
|
||||||
base_model,
|
base_model,
|
||||||
model_to_quantize,
|
model_to_quantize,
|
||||||
ara_path=base_model.model_config.accuracy_recovery_adapter,
|
ara_path=mc.accuracy_recovery_adapter,
|
||||||
exclude=exclude_modules,
|
exclude=exclude_modules,
|
||||||
|
device=device,
|
||||||
|
keep_on_device=keep_on_device,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_quantize_model_blocks(base_model, model_to_quantize, exclude_modules)
|
quantize_module(
|
||||||
|
model_to_quantize,
|
||||||
|
mc.qtype,
|
||||||
|
device=device,
|
||||||
|
dtype=base_model.torch_dtype,
|
||||||
|
block_names=base_model.get_transformer_block_names(),
|
||||||
|
exclude=exclude_modules,
|
||||||
|
quantize_kwargs=mc.quantize_kwargs,
|
||||||
|
status_fn=base_model.print_and_status_update,
|
||||||
|
keep_on_device=keep_on_device,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
@@ -412,11 +449,18 @@ def attach_ara_and_quantize(
|
|||||||
model_to_quantize: torch.nn.Module,
|
model_to_quantize: torch.nn.Module,
|
||||||
ara_path: str,
|
ara_path: str,
|
||||||
exclude: Optional[List[str]] = None,
|
exclude: Optional[List[str]] = None,
|
||||||
|
device=None,
|
||||||
|
keep_on_device: bool = False,
|
||||||
):
|
):
|
||||||
"""Load an accuracy recovery adapter as a live network on the module and
|
"""Load an accuracy recovery adapter as a live network on the module and
|
||||||
quantize around it (adapter-hijacked linears at the configured qtype,
|
quantize around it (adapter-hijacked linears at the configured qtype,
|
||||||
everything else uint8). The network lands on
|
everything else uint8). The network lands on
|
||||||
base_model.accuracy_recovery_adapter."""
|
base_model.accuracy_recovery_adapter.
|
||||||
|
|
||||||
|
``device``: quantize each hijacked linear there (gpu kernels) instead of
|
||||||
|
wherever it happens to live (historically the cpu — slow). With
|
||||||
|
``keep_on_device`` the quantized linears stay there (final home is that
|
||||||
|
gpu); otherwise each returns to the device it came from."""
|
||||||
from toolkit.config_modules import NetworkConfig
|
from toolkit.config_modules import NetworkConfig
|
||||||
from toolkit.lora_special import LoRASpecialNetwork
|
from toolkit.lora_special import LoRASpecialNetwork
|
||||||
|
|
||||||
@@ -537,12 +581,19 @@ def attach_ara_and_quantize(
|
|||||||
base_model.accuracy_recovery_adapter = network
|
base_model.accuracy_recovery_adapter = network
|
||||||
|
|
||||||
# quantize it
|
# quantize it
|
||||||
|
keep_on_device = keep_on_device and device is not None
|
||||||
lora_exclude_modules = []
|
lora_exclude_modules = []
|
||||||
quantization_type = get_qtype(base_model.model_config.qtype)
|
quantization_type = get_qtype(base_model.model_config.qtype)
|
||||||
for lora_module in tqdm(network.unet_loras, desc="Attaching quantization"):
|
for lora_module in tqdm(network.unet_loras, desc="Attaching quantization"):
|
||||||
# the lora has already hijacked the original module
|
# the lora has already hijacked the original module
|
||||||
orig_module = lora_module.org_module[0]
|
orig_module = lora_module.org_module[0]
|
||||||
orig_module.to(base_model.torch_dtype)
|
orig_device = None
|
||||||
|
if device is not None:
|
||||||
|
param = next(orig_module.parameters(), None)
|
||||||
|
orig_device = param.device if param is not None else None
|
||||||
|
orig_module.to(device, dtype=base_model.torch_dtype)
|
||||||
|
else:
|
||||||
|
orig_module.to(base_model.torch_dtype)
|
||||||
# make the params not require gradients
|
# make the params not require gradients
|
||||||
for param in orig_module.parameters():
|
for param in orig_module.parameters():
|
||||||
param.requires_grad = False
|
param.requires_grad = False
|
||||||
@@ -550,78 +601,23 @@ def attach_ara_and_quantize(
|
|||||||
freeze(orig_module)
|
freeze(orig_module)
|
||||||
module_name = lora_module.lora_name.replace('$$', '.').replace('transformer.', '')
|
module_name = lora_module.lora_name.replace('$$', '.').replace('transformer.', '')
|
||||||
lora_exclude_modules.append(module_name)
|
lora_exclude_modules.append(module_name)
|
||||||
if base_model.model_config.low_vram:
|
if not keep_on_device and orig_device is not None:
|
||||||
# move it back to cpu
|
orig_module.to(orig_device)
|
||||||
|
elif base_model.model_config.low_vram and device is None:
|
||||||
|
# legacy behavior when no quantize device was given
|
||||||
orig_module.to("cpu")
|
orig_module.to("cpu")
|
||||||
pass
|
|
||||||
# quantize additional layers
|
# quantize additional layers
|
||||||
print_acc(" - quantizing additional layers")
|
print_acc(" - quantizing additional layers")
|
||||||
quantization_type = get_qtype('uint8')
|
quantization_type = get_qtype('uint8')
|
||||||
quantize(
|
quantize(
|
||||||
model_to_quantize,
|
model_to_quantize,
|
||||||
weights=quantization_type,
|
weights=quantization_type,
|
||||||
exclude=lora_exclude_modules + exclude_modules
|
exclude=lora_exclude_modules + exclude_modules,
|
||||||
|
quantize_device=device,
|
||||||
|
keep_on_quantize_device=keep_on_device,
|
||||||
)
|
)
|
||||||
|
if keep_on_device:
|
||||||
|
# non-quantized leftovers follow — final residency, not a transient
|
||||||
|
model_to_quantize.to(device)
|
||||||
|
|
||||||
|
|
||||||
def _quantize_model_blocks(base_model, model_to_quantize, exclude_modules):
|
|
||||||
if True:
|
|
||||||
# quantize model the original way without an accuracy recovery adapter
|
|
||||||
# move and quantize only certain pieces at a time.
|
|
||||||
quantization_type = get_qtype(base_model.model_config.qtype)
|
|
||||||
quantize_kwargs = base_model.model_config.quantize_kwargs or {}
|
|
||||||
# all_blocks = list(model_to_quantize.transformer_blocks)
|
|
||||||
all_blocks: List[torch.nn.Module] = []
|
|
||||||
transformer_block_names = base_model.get_transformer_block_names() or []
|
|
||||||
for name in transformer_block_names:
|
|
||||||
# name may be a dotted path for models that nest their blocks
|
|
||||||
# (e.g. hidream_o1's "model.language_model.layers").
|
|
||||||
block_list = model_to_quantize
|
|
||||||
for part in name.split('.'):
|
|
||||||
block_list = getattr(block_list, part, None)
|
|
||||||
if block_list is None:
|
|
||||||
break
|
|
||||||
if block_list is not None:
|
|
||||||
all_blocks += list(block_list)
|
|
||||||
base_model.print_and_status_update(
|
|
||||||
f" - quantizing {len(all_blocks)} transformer blocks"
|
|
||||||
)
|
|
||||||
already_quantized = 0
|
|
||||||
for block in tqdm(all_blocks):
|
|
||||||
if not _has_quantizable_linear(block, quantization_type, exclude_modules):
|
|
||||||
# pre-quantized checkpoint with a matching qtype: nothing in
|
|
||||||
# this block would change — skip the device round-trip and the
|
|
||||||
# dtype cast entirely so the load stays byte-identical
|
|
||||||
already_quantized += 1
|
|
||||||
continue
|
|
||||||
block.to(base_model.device_torch, dtype=base_model.torch_dtype, non_blocking=True)
|
|
||||||
# exclude patterns with a leading wildcard (e.g. "*adaln_proj*")
|
|
||||||
# also apply inside blocks, where names are block-relative
|
|
||||||
quantize(
|
|
||||||
block,
|
|
||||||
weights=quantization_type,
|
|
||||||
exclude=exclude_modules,
|
|
||||||
**quantize_kwargs,
|
|
||||||
)
|
|
||||||
freeze(block)
|
|
||||||
# NOT non_blocking: an async D2H allocates the cpu destination in pinned
|
|
||||||
# memory, which the caching host allocator keeps forever (with power-of-2
|
|
||||||
# bucket rounding on top) — that silently retained a model-sized chunk of
|
|
||||||
# host ram after the weights moved back to the gpu for training
|
|
||||||
block.to("cpu")
|
|
||||||
if already_quantized:
|
|
||||||
base_model.print_and_status_update(
|
|
||||||
f" - {already_quantized} blocks already quantized with a matching qtype; left untouched"
|
|
||||||
)
|
|
||||||
|
|
||||||
# todo, on extras find a universal way to quantize them on device and move them back to their original
|
|
||||||
# device without having to move the transformer blocks to the device first
|
|
||||||
base_model.print_and_status_update(" - quantizing extras")
|
|
||||||
# model_to_quantize.to(base_model.device_torch, dtype=base_model.torch_dtype)
|
|
||||||
quantize(
|
|
||||||
model_to_quantize,
|
|
||||||
weights=quantization_type,
|
|
||||||
exclude=exclude_modules,
|
|
||||||
**quantize_kwargs,
|
|
||||||
)
|
|
||||||
freeze(model_to_quantize)
|
|
||||||
Reference in new issue
Block a user