[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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

[cuda backend] share AOTI weights by FQN across methods - #21823

Merged
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing
Aug 26, 2026
Merged

[cuda backend] share AOTI weights by FQN across methods#21823
Gasoonjia merged 12 commits into
mainfrom
codex/cuda-fqn-weight-sharing

Conversation

@Gasoonjia

Copy link
Copy Markdown
Contributor

Summary

Currently we store weight in method granularity: if two methods do not share identical weight we have to keep two weight copies, even if they may just one tensor different.

This PR stores the weight in tensor granularity: we store the weight pickle format and use FQN as the key for each tensor. With this PR weight can be shared across different methods even if they are partially overlapped.

@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/21823

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

✅ You can merge normally! (2 Unrelated Failures)

As of commit fa7c98a with merge base d2f97d5 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from 071b477 to b7d877fCompareAugust 13, 2026 22:26
@Gasoonjia
Gasoonjiaforce-pushed the codex/cuda-fqn-weight-sharing branch from b7d877f to 0b7f1cbCompareAugust 19, 2026 04:00
@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 3m 54s —— View job


PR Review — share AOTI weights by FQN across methods

Nice work — this is a substantial and well-reasoned change. The manifest format, overflow-hardened view validation, content-addressed dedup, and the mutable/immutable sharing distinction are all thoughtfully done. Below are findings grouped by severity.

🟠 Correctness / robustness

1. Thread-local capture is fragile if AOTI materializes on a worker thread
preprocess sets _FQN_WEIGHTS_CAPTURE.current and relies on materialize_weights_blob reading it back on the same thread (cuda_backend.py:685-692, 785-789). If super().preprocess (AOTI packaging) ever invokes materialize_weights_blob from a worker/pool thread, capture is None and the export dies with "CUDA structured weights must be materialized inside preprocess". It works today because packaging is synchronous, but this couples correctness to an AOTI implementation detail. Worth a comment documenting the same-thread requirement, or threading the capture through explicitly rather than via TLS.

2. get_key() pointer identity as the mutable-scope key is a subtle assumption
acquire_fqn_weight_storage (cuda_backend.cpp:1069-1099) uses reinterpret_cast<uintptr_t>(key.get()) as the PTD-instance identity for scoping mutable weights. The comment explains the reasoning well, but this leans on get_key() returning a pointer owned by the underlying PTD map and stable for its lifetime. That's an implementation contract of NamedDataMap/MergedDataMap, not an interface guarantee — if an implementation ever returns a pointer into a temporary or a per-call buffer, mutable state would silently stop being shared (or worse, cross-contaminate after address reuse). Consider asserting/documenting this contract at the NamedDataMap interface, or deriving the scope from something with a documented lifetime.

3. Unsupported dtypes fail at runtime rather than export
_materialize_fqn_weights encodes scalar_type_enum(tensor.dtype) for any dtype, but the runtime whitelist is_supported_cuda_fqn_dtype / validate_fqn_weight_view only accepts a fixed set (no float64, complex, float8, etc.). A model with an unsupported constant dtype will export cleanly and only fail with InvalidProgram at load time. Validating against the supported set in _materialize_fqn_weights (or _encode_fqn_weight_manifest) would surface a clear error at export.

🟡 Minor / performance

4. O(entries × keys) scan for mutable weights
The get_num_keys()/get_key() linear scan in acquire_fqn_weight_storage runs once per non-shareable entry. For a model with many mutable buffers this is quadratic in named-data keys. Since the scan only maps storage_key → key pointer, you could build that map once per load_constants_from_fqn_manifest call and reuse it. Fix this →

5. Comment placement between if and else if
cuda_backend.cpp:433-439: the multi-line legacy comment now sits after the closing brace of the if (has_fqn_weights) block and before else if, so it reads as documentation for a branch it's separated from. Moving it directly above the else if (or into the block) would read more clearly.

6. Behavior change scope — confirm intended
_weights_format now unconditionally returns "pickle_weights" and materialize_weights_blob dropped its _is_low_memory_mode guard (cuda_backend.py:766, 1065-1071). So the FQN path now applies to all CUDA exports, not just low-memory mode. This matches the PR intent ("backend-wide"), but it's a broader behavior change than the title implies — please confirm non-low-memory paths were exercised end-to-end (the determine_aoti_mmap_flags patch still only forces the external-weights ABI under _is_cpu_clone_active(), so it'd be good to verify the pickle_weights + non-low-memory combination produces a correctly loadable artifact).

🟢 Things I liked / verified

  • Overflow-safe span computation in validate_fqn_weight_view (cuda_backend.cpp:1063-1097) mirrors the Python-side required_nbytes check — good defense in depth.
  • Content-addressed storage_key (sha256 + "_cuda_weight_storage") correctly dedups identical immutable storages while keeping identical mutable buffers in distinct groups — nicely covered by test_identical_mutable_storages_remain_distinct_groups.
  • CudaWeightStorage's destructor restores the prior CUDA device around cudaFree — correct for multi-GPU.
  • weak_ptr cache with size re-validation on hit avoids stale-alias bugs; expired entries fall through to a fresh allocation.
  • Error paths in acquire_fqn_weight_storage consistently cudaFree before returning. host_data->Free() is called after the H2D copy.
  • Manifest parser is strict (magic check, entry cap, dim cap, trailing-byte rejection) and the new test_cuda_weight_manifest.cpp covers legacy-misdetection, truncation, trailing data, and unsupported dtype.

Suggestions

  • Add a runtime/integration test that actually loads a two-method model sharing a partial weight set and asserts the "reused across methods" path (the log line at cuda_backend.cpp:1454), so the core value prop is regression-covered beyond the parser unit tests.
  • Consider centralizing the dtype↔size table: is_supported_cuda_fqn_dtype, the item_size switch in validate_fqn_weight_view, and the Python whitelist are three places that must stay in sync.

Overall the design is sound and the safety checks are thorough. The main things I'd want resolved before merge are #1 (TLS coupling) and #3 (late dtype failure); #2 is worth at least a documented contract.
· branch codex/cuda-fqn-weight-sharing

Comment threadbackends/cuda/runtime/cuda_weight_cache.h
@linux-foundation-easycla

linux-foundation-easyclaBot commented Aug 25, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Generated with Codex.
Move structured CUDA weight collection into a dedicated export-side collector, key values by device and FQN, and reject conflicting values or metadata. Keep the runtime backend integration small by delegating manifest loading and cross-method allocation reuse to a focused CUDA weight cache.\n\nGenerated with Codex.
Keep serialized FQN metadata parsing, validation, allocation, and AOTI binding behind CudaWeightCache, and remove the standalone manifest abstraction.\n\nGenerated with Codex.
Split named-data merging and FQN registration into focused collector helpers to satisfy complexity lint without changing serialization behavior.\n\nGenerated with Codex.
Generated with Codex.
@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

/easycla

Comment on lines -412 to -413
// methods are independent sub-graphs that may have FQN collisions
// (e.g. parakeet).

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.

what about this, how do we handle this if the fqns are same but content is indeed different?

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.

Right now we just raised error: from eager model perspective different methods are different "forward" functions in a same model, which means they shouldn't contain weights sharing same fqn.

@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR.

@digantdesaidigantdesai left a comment

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.

what were the memory footprint gains?

@claude

claudeBot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 48s —— View job


PR Review — share AOTI weights by FQN across methods

Focused on @digantdesai's two open questions, plus what changed since my last review.


❓ "How do we handle same FQN but different content?" (cuda_backend.cpp:413)

There are two distinct cases, handled in different places:

1. Anonymous library-local constants (_tensor_constant*). These are the dangerous ones — PyTorch restarts the numbering in every independently-compiled AOTI library, so _tensor_constant0 in method A and _tensor_constant0 in method B are unrelated tensors that just happen to share a name. The PR namespaces them by the owning library so they can never collide (cuda_weight_collector.py:110-114, 337-343):

def_is_aoti_library_local_fqn(fqn: str) ->bool:
returnfqn.startswith("_tensor_constant")
...
if_is_aoti_library_local_fqn(entry.fqn):
entry=replace(entry, storage_key=_storage_key(
entry.fqn, entry.device_type, aoti_library_key=so_blob_key))

The so_blob_key is a content hash of the .so, so each library gets its own storage key → distinct-content-under-same-name stays distinct. This is exactly what the old parakeet fallback protected against, now handled structurally instead of by disabling sharing.

2. Genuine model-level FQNs (e.g. encoder.weight). The storage key is cuda_fqn_weight:{device}:{fqn}, and the bytes are written through NamedDataStore.add_named_data. That store is content-verified (_named_data_store.py:141-150): same key + identical bytes → deduped to one buffer; same key + different bytes → hard ValueError("Duplicate key … with different data") at export time. So a real FQN collision with divergent content fails loudly at export rather than silently binding the wrong tensor. That's the correct default — a shared model-level FQN that legitimately holds different tensors across methods is a genuine conflict, and content-addressed keying surfaces it instead of guessing.

At runtime the same guarantee holds from a different angle: acquire_storage keys its GPU-allocation cache on storage_key @ logical_scope @ device (cuda_weight_cache.cpp:297-299), and re-validates nbytes/device on a cache hit (308-313) — so a stale or mismatched alias can never be reused.

One thing worth confirming: the _tensor_constant detection is a prefix string match. If a real model ever has a user-named parameter starting with _tensor_constant, it would be incorrectly library-scoped. Very unlikely, but a comment noting this is a PyTorch-internal naming contract (not user-facing) would be worth it.


❓ "What were the memory footprint gains?"

I can't run the benchmark from here, but the mechanism gives a clear bound. Previously weights were stored at method granularity — two methods sharing all-but-one tensor still kept two full copies, both on disk (.ptd) and in GPU memory. Now storage is tensor granularity, deduped in two places:

  • On disk:NamedDataStore dedups identical storages to a single buffer (content-addressed).
  • In GPU memory: the runtime weak_ptr cache (storages_) hands the same CudaWeightStorage to every method that references a given (storage_key, scope, device).

So for a prefill/decode pair sharing S bytes of the T-byte weight set, the saving is roughly (num_methods − 1) × S on both axes; a partial overlap now saves the overlapping subset instead of nothing. It'd strengthen the PR to drop the measured before/after .ptd size and peak GPU memory for the model that motivated this into the description.


🔎 Re-review — status of prior findings

  • Container/library leak (cuda_backend.cpp:872-885): good catch keeping the .so loaded — the leaked container still owns code/state in that library, so dlclose during multi-method teardown was a real crash risk. Comment explains it well.
  • Overflow-hardened view validation (cuda_weight_cache.cpp:205-277) and strict manifest parser (magic, entry cap, dim cap, trailing-byte rejection) are retained and solid.
  • Scope identity via get_key() pointer is now hoisted into a key_scopes map built once per load() (cuda_weight_cache.cpp:462-477) — this also resolves my prior perf note (Add unlifting pass under private config #4) about the O(entries×keys) scan. Nice.
  • 🟠 Unsupported dtypes still fail at runtime, not export.materialize encodes scalar_type_enum(tensor.dtype) for any dtype (cuda_weight_collector.py:253), but the runtime whitelist is_supported_dtype (cuda_weight_cache.cpp:118-133) rejects float64/complex/float8/etc. A model with such a constant exports cleanly and only fails at load. Validating against the supported set in materialize would surface a clear export-time error. Fix this →
  • 🟡 Three-place dtype↔size table.is_supported_dtype (enum list), the item_size switch in validate_view, and the (proposed) Python whitelist must stay in sync — worth a shared source of truth or at least a cross-referencing comment.
  • 🟡 TLS same-thread coupling remains.materialize_weights_blob reads CudaWeightCollector.current_capture() from thread-local state set in preprocess (cuda_backend.py:531-540, cuda_weight_collector.py:169-176). Correct as long as AOTI packaging stays synchronous on the calling thread; current_capture() raises a clear error otherwise, so it fails safe. A one-line comment documenting the same-thread contract would help.
  • 🟡 Behavior is now backend-wide._weights_format unconditionally returns "pickle_weights" and materialize_weights_blob dropped its low-memory guard, so the FQN path applies to all CUDA exports. This matches the PR intent, but note _force_external_weights_for_fqn_binding only forces the external-weights ABI under _is_cpu_clone_active() (cuda_backend.py:207-217) — good to confirm the pickle_weights + non-low-memory combo produces a loadable artifact end-to-end.

Overall: the design is sound and the two hardest correctness concerns (anonymous-constant collisions, stale GPU aliasing) are handled structurally. Nothing here is blocking given the approval — the dtype-at-export item is the main one I'd still land.
· branch codex/cuda-fqn-weight-sharing

@Gasoonjia

Copy link
Copy Markdown
ContributorAuthor

what were the memory footprint gains?

no obvious memory footprint change, but ptd size reduced a lot

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