在 Windows 4060Ti显存16G的消费级显卡上跑通文生视频LTX-2.3 22B大模型 disk offload 修复说明
本文档记录了如何让 LTX-2.3 的 22B 模型(46GB) 在
16GB 显存 + 32GB 内存的 Windows 机器上跑通,以及为此对 ltx-core 做的 4 处代码修改。
受众人群: 想在显存/内存不足的 Windows 机器上做模型推理、并想理解这些改动原理与通用性的人。
1. 背景:为什么原本跑不起来
1.1 硬件与模型的矛盾
| 项目 | 数值 |
|---|---|
模型 ltx-2.3-22b-distilled-1.1.safetensors |
46 GB(bf16) |
| fp8-cast 量化后 | 约 27.6 GB |
| 4bit 量化后(理论) | 约 12 GB |
| 本机显存(RTX 4060 Ti) | 16 GB |
| 本机物理内存 | 32 GB(空闲约 23 GB) |
--offload none(全部进显存)→ 需 27GB+,显存装不下。--offload cpu(权重 pin 到内存)→ 需把约 22–27GB 权重 pin 进锁页内存,32GB 内存 pin 不下。--offload disk(从磁盘按需流式读取,只在 GPU 上保留 2 个块)→ 这是唯一显存/内存占用都低的路径,但在 Windows 上崩溃。
--offload disk 的原理是:transformer 的每个 block 权重平时留在磁盘上,采样时后台线程逐块从磁盘读入、拷到 GPU,用完即换出。GPU 上任意时刻只驻留 2 个 block,所以显存占用极低(实测约 6.5GB)。代价是慢(每步要读盘)。
1.2 崩溃现象
--offload disk 在 Windows 上按顺序暴露了 4 个不同的崩溃/挂起,逐个修复后才跑通:
- 进程挂死(CPU 停摆、内存归零)——
cudaHostRegister锁页大块内存卡死。 - 段错误(access violation)——从 pageable 内存做异步 H2D 拷贝。
- 段错误——safetensors 直接读入 CUDA。
- 段错误——两个 safetensors mmap 句柄同时打开同一个大文件(根因)。
2. 四处修改详解
所有修改都由环境变量 LTX_DISABLE_PINNED=1 统一开启(见 run.bat)。不设该变量时,行为与官方原版完全一致——所以这些改动对 Linux/其他平台零影响。
修改 1:block_streaming/utils.py — 跳过会挂死的 cudaHostRegister
问题:pinned(锁页)内存的分配走 _alloc_pinned_exact(),它调用 CUDA 的 cudaHostRegister 把一块主机内存"锁页"。在 Windows 上,对较大内存区域调用 cudaHostRegister 会同步遍历整个区域做页锁定,可能耗时数分钟甚至挂死。
修改:在 alloc_buffer() 开头加一个开关,LTX_DISABLE_PINNED=1 时直接不 pin,退回普通(pageable)内存。
def alloc_buffer(nbytes: int, device: torch.device | None, pin_memory: bool) -> torch.Tensor:
if pin_memory and os.environ.get("LTX_DISABLE_PINNED") == "1":
pin_memory = False # ← 新增:Windows 上跳过 cudaHostRegister
if pin_memory and not torch.cuda.is_available():
pin_memory = False
...
代价:pinned 内存的作用是让 H2D 拷贝可以异步(与计算重叠)。不 pin 就只能同步拷贝,速度略降,但不影响正确性。
修改 2:block_streaming/stream_sync.py — 禁 pin 时改用同步拷贝
问题:一旦内存不是 pinned(pageable),CUDA 的异步(non_blocking=True)H2D 拷贝是未定义行为,在 Windows 上直接段错误。而 CudaStreamSync.is_async_copy 原本恒为 True。
修改:禁 pin 时把异步拷贝整体关掉,拷贝改在默认流上同步执行;相关的跨流 event 也退化成 None(同步拷贝完成后 buffer 立即可复用,无需 event 守卫)。
def __init__(self, device):
...
# pageable 内存 + 异步 H2D 在 Windows 上段错误;禁 pin 时强制同步拷贝
self._async = os.environ.get("LTX_DISABLE_PINNED") != "1"
@property
def is_async_copy(self) -> bool:
return self._async
def copy_scope(self):
if not self._async:
return contextlib.nullcontext() # 不再切到独立 copy stream
return torch.cuda.stream(self._copy_stream)
def commit_copy(self):
if not self._async:
return None # 同步拷贝已完成,无需 event
...
代价:失去 copy/compute 重叠,略慢。正确性不受影响。
修改 3:loader/sft_loader.py — CPU 打开 + clone 断开 mmap + 同步拷贝
问题(两个):
safe_open(..., device="cuda")让 safetensors 直接把权重读进显存,在 Windows 上get_tensor内部访问越界。- 即便读到 CPU,把一个 storage 是 safetensors mmap 的张量直接
.to("cuda"),在 Windows 上也会在 storage 切片处段错误。
修改:禁 pin 时,①在 CPU 上打开;②先 .clone() 让张量脱离 mmap 存储,拿到普通内存;③再做同步 H2D。
# 定义在文件顶部
_NON_BLOCKING = os.environ.get("LTX_DISABLE_PINNED") != "1"
# load() 内
open_device = "cpu" if not _NON_BLOCKING else str(device)
with safetensors.safe_open(shard_path, framework="pt", device=open_device) as f:
for name in f.keys():
...
value = f.get_tensor(name)
if not _NON_BLOCKING:
value = value.clone() # ← 脱离 mmap 存储
value = value.to(device=device, non_blocking=_NON_BLOCKING, copy=False)
代价:clone() 多一次内存拷贝,略增内存峰值(单张量级别,可忽略)。
修改 4(根因):block_streaming/disk.py — DiskTensorReader 延迟打开句柄
问题(真正的根因):disk offload 运行时,两个 safetensors mmap 句柄同时打开同一个 46GB 文件:
- 一个是 block streaming 的
DiskTensorReader(读 transformer 块权重); - 另一个是
sft_loader加载 non-block 权重(VAE、embedding 等)时打开的。
经最小复现验证:在 Windows 上,只要对同一大文件同时存在两个 safe_open 句柄,读取时就段错误(与是否用 CUDA、是否并发读无关——两个句柄"共存"即崩)。而顺序打开(任一时刻只有一个句柄)完全正常。
修改:让 DiskTensorReader 延迟打开——构造时只用一个短生命周期句柄读一遍 key 列表就关掉;真正的长生命周期 mmap 句柄推迟到首次 get_tensor(即块 fetch)时才打开。而块 fetch 发生在 non-block 权重加载之后,于是两个句柄不再共存。
class DiskTensorReader:
def __init__(self, paths):
self._paths = list(paths)
self._handles = None # ← 延迟打开
self._key_to_handle_idx = {}
# 只用短命句柄读一次 key 列表,读完立即关闭
for handle_idx, path in enumerate(self._paths):
with safetensors.safe_open(path, framework="pt", device="cpu") as handle:
for sft_key in handle.keys():
self._key_to_handle_idx[sft_key] = handle_idx
def _ensure_open(self):
if self._handles is None: # ← 首次 get_tensor 才真正打开
self._handles = [safetensors.safe_open(p, framework="pt", device="cpu")
for p in self._paths]
return self._handles
def get_tensor(self, key):
handles = self._ensure_open()
return handles[self._key_to_handle_idx[key]].get_tensor(key)
代价:几乎为零。仅把长生命周期句柄的打开时机往后挪。
3. 复现命令
关键验证脚本(证明根因是"两个句柄共存"):
# 崩溃:两个句柄同时打开同一大文件,纯读、不碰 CUDA 也段错误
import safetensors
p = "models/ltx-2.3-22b-distilled-1.1.safetensors"
h1 = safetensors.safe_open(p, framework="pt", device="cpu")
h2 = safetensors.safe_open(p, framework="pt", device="cpu")
for k in h2.keys():
if ".transformer_blocks." not in k:
h2.get_tensor(k) # ← 这里 access violation
# 正常:顺序打开(关掉 h1 再开 h2),即使 .to("cuda") 也没问题
h1 = safetensors.safe_open(p, framework="pt", device="cpu"); del h1
h2 = safetensors.safe_open(p, framework="pt", device="cpu")
for k in h2.keys():
if ".transformer_blocks." not in k:
h2.get_tensor(k).clone().to("cuda") # ← 全部成功
4. 运行方式
run.bat 已配置好:
@echo off
set LTX_DISABLE_PINNED=1
uv run python -m ltx_pipelines.distilled ^
--distilled-checkpoint-path "models/ltx-2.3-22b-distilled-1.1.safetensors" ^
--spatial-upsampler-path "models/ltx-2.3-spatial-upscaler-x2-1.1.safetensors" ^
--gemma-root "models/gemma-3-12b" ^
--offload disk ^
--quantization fp8-cast ^
--height 512 --width 320 --num-frames 121 ^
--seed 42 --output-path "output.mp4" ^
--prompt "..."
实测:GPU 占用约 6.5GB,采样每步约 27 秒。质量为满血 22B 无损。
5. 这些修改是否具有通用性?
部分通用,需要分开看。
✅ 通用的部分
- 根因(修改 4)是通用的。"Windows 上同一大文件的多个 safetensors mmap 句柄并发会崩"是 safetensors/torch 在 Windows 上的底层行为,与 LTX 无关。任何在 Windows 上、对大 checkpoint 同时开多个
safe_open句柄的项目(尤其是 offload / 分片流式加载类)都可能踩到,延迟打开/顺序化句柄的思路可直接借鉴。 - 修改 1/2/3 针对的三类问题(大块
cudaHostRegister挂死、pageable 内存异步 H2D 段错误、mmap 张量直接.to(cuda)崩)都是 Windows + CUDA 的共性坑,在其他做手动内存 offload 的推理框架里同样成立。原理层面完全可迁移。
⚠️ 不能直接照搬的部分
- 这些补丁是针对 LTX-2.3 的
ltx-core代码结构写的(具体函数名、LTX_DISABLE_PINNED开关、调用时序)。换一个项目要重新定位对应代码位置,不能原样复制文件。 - 修改 1/2 是用速度换稳定(放弃 pinned 内存和异步拷贝)。在 pinned 内存工作正常的平台(Linux)上不该开启,否则白白损失性能——所以用环境变量做成"仅 Windows/需要时启用"。
- 只在这一套硬件 + 这个模型上验证过。不同显存/内存、不同模型大小,阈值和是否需要 disk offload 会变。
❌ 不通用/局限
- 治标不治本:根本矛盾仍是"16GB 显存跑 46GB 模型"。disk offload 只是让它能跑,不是跑得快——每步读盘,速度受磁盘 IO 限制。想要快,正解仍是更大显存,或对模型做真正的 4bit 量化(把模型压到能整体进显存)。
- 官方未来若重构加载器或升级 safetensors 修复了 Windows mmap 问题,这些补丁可能需要重新适配或不再需要。
建议
- 上游价值最高的是修改 4。可以考虑给
ltx-core/ safetensors 提 issue 或 PR:说明 Windows 上并发 mmap 句柄的问题,并附上第 3 节的最小复现。这对社区最有用。 - 其余 3 处更像"Windows 兼容性开关",适合以
LTX_DISABLE_PINNED这类可选开关的形式长期保留,而非改变默认行为。
更多推荐



所有评论(0)