Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix CUDA low-memory external weights export - #21798

Merged
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix
Aug 20, 2026
Merged

Fix CUDA low-memory external weights export#21798
Gasoonjia merged 4 commits into
mainfrom
low-host-mem-cuda-export-fix

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

  • Stream AOTInductor external weights to disk instead of materializing a model-sized bytes object in host memory.
  • Keep the resulting weights blob file-backed during PTE serialization.
  • Avoid deep-copying immutable tensor storage for each delegated method.
  • Move compile-time clones of mutated buffers to CPU and temporarily release zero-initialized KV-cache storage.
  • Preserve the existing export path unless low_memory_mode is enabled.

On MG 30B, peak host RSS decreased from 75.2 GiB to 48.8 GiB, a 26.3 GiB (35.1%) reduction. A separate 131K-context export completed with approximately 17 GiB peak GPU.

Testing

  • Added CUDA low-memory export and serialization unit tests.
  • Verified full MG 30B export and runtime successfully.

@pytorch-bot

pytorch-botBot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21798

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR plz

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 8m 19s —— View job


PR Review: Fix CUDA low-memory external weights export

  • Read the diff (cuda_backend.py, test_cuda_partitioner.py)
  • Cross-check base-class contracts (aoti_backend.py) and runtime consumer (cuda_backend.cpp)
  • Verify FileBackedData / NamedDataStore integration
  • Assess correctness, edge cases, and test coverage

Overall this is a well-structured change. Every new behavior is gated behind low_memory_mode="ON" and the default export path is untouched, the streaming writer genuinely avoids the model-sized bytes object, and the FileBackedData handoff into NamedDataStore.add_named_data is type-compatible (_named_data_store.py accepts bytes | FileBackedData | Tensor). Nice work, and the docstrings carry the non-obvious rationale well.

A few things worth confirming or tightening before merge.

1. Blob alignment: the production (all-CUDA) path is packed, but only the padded path is unit-tested

In _write_aoti_weights_blob (backends/cuda/cuda_backend.py:339) the 64-byte inter-tensor padding is written only when not all_cuda:

ifnotall_cudaand (padding:= (-nbytes) %64):
output.write(bytes(padding))

So the real MG 30B export (all constants on CUDA) writes tensors back-to-back with no alignment padding, while the CPU/mixed path pads each tensor to a 64-byte boundary. The blob's constant offsets are baked into the AOTI-generated wrapper at compile time (update_constants_from_blob in cuda_backend.cpp:1105), so the streamed layout must match exactly what the wrapper expects — a mismatch produces silently garbage weights, not a crash.

The docstring claims "same layout as AOTInductor's binary_blob", and the PR says the full MG 30B export + runtime were verified, which is the real evidence here. But test_low_memory_weights_are_streamed_in_binary_blob_format uses CPU tensors, so it only exercises the padded branch — the packed all-CUDA branch that production actually takes has no unit coverage. Two suggestions:

  • Add a short comment explaining why CUDA constants are packed while CPU/mixed are 64-byte aligned (this asymmetry is surprising — layout is normally a compile-time property independent of where the source tensor currently lives).
  • If feasible in CI, add a test asserting the packed (no-padding) layout for the CUDA case, or at least document that it's validated only via the end-to-end MG 30B export.

2. next(...) raises an opaque StopIteration if no .so is present

backends/cuda/cuda_backend.py:491:

so_path=next(
pathforpathinpathsifisinstance(path, str) andpath.endswith(".wrapper.so")
)

If Inductor's outputs ever lack a .wrapper.so, this raises a bare StopIteration rather than a descriptive error — inconsistent with the explicit RuntimeError you raise two lines up for the weights count. Consider a next(..., None) + explicit raise RuntimeError(f"...got {paths}"). Fix this →

3. _codecache.TensorProperties is patched process-globally, not gated by the thread-local guard

In _compile_time_cpu_clones (:188), determine_aoti_mmap_flags is wrapped so it only diverges when _is_cpu_clone_active() (thread-local) is true, but TensorProperties is replaced with an unconditional functools.partial. The blast radius is small — _tensor_properties_for_low_memory only diverges (returns None) for emptied tensors and otherwise delegates — so a concurrent export in another thread is unlikely to be affected. Still, for consistency with the other patches, consider gating it on _is_cpu_clone_active() too. Minor.

4. Minor / nits

  • chunk_size = 8 * 1024 * 1024 (:317) duplicates FileBackedData._COPY_CHUNK_SIZE; could reference the shared constant.
  • all_cuda = all(...) is True for an empty tensors list (:316) → no padding on an empty weights set. Harmless, but the len(weights) != 1 guard already makes empty weights impossible to reach here, so it's moot.
  • The shared-storage deepcopy in copy_exported_program_for_preprocess (:521) is safe becausemove_program_to_device and release_moved_tensors only replace state-dict entries (via .to() / _empty_strided_on_device) and resize the new device tensors — they never resize_(0) the shared source storage in place. That invariant is load-bearing for correctness across multi-method exports; worth a one-line note at the call site so a future change to release_moved_tensors doesn't silently corrupt the shared source tensors.

None of these are blockers. #1 is the one I'd most like confirmed, since it's the highest-consequence correctness point and the only one not covered by the unit tests.
• branch low-host-mem-cuda-export-fix

raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
raw_view = memoryview(raw_array).cast("B")
for offset in range(0, nbytes, chunk_size):
output.write(raw_view[offset : offset + chunk_size])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any noticeable increase in export time because of all these writes?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the export time remains the same

@Gasoonjia
Gasoonjia merged commit 7d84f08 into mainAug 20, 2026
222 checks passed
@Gasoonjia
Gasoonjia deleted the low-host-mem-cuda-export-fix branch August 20, 2026 18:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cudaCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Gasoonjia@digantdesai