[NVBug: 5987078] Fix unified HF export of compressed NVFP4 weights (--low_memory_mode) - #2038
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe Hugging Face exporter now supports compressed NVFP4 weights. It reuses and rescales stored compression scales, preserves dequantization, removes internal quantizer buffers, and adds GPU regression coverage. ChangesCompressed NVFP4 export
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant QTensorWrapper
participant unified_export_hf
participant TensorRTLLM
participant Checkpoint
QTensorWrapper->>unified_export_hf: Provide compressed NVFP4 weights and stored scales
unified_export_hf->>TensorRTLLM: Convert swizzled block scales when required
unified_export_hf->>unified_export_hf: Rescale block scales for weight_scale_2
unified_export_hf->>Checkpoint: Write exported weights without internal scale buffers
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
modelopt/torch/export/unified_export_hf.py (1)
668-679: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSkip the temporary packed-weight scale calculation.
get_weight_scaling_factor(...)already runs before this code detectsQTensorWrapper. For a compressed NVFP4 weight, Lines 716-720 replace that result. Detect the compressed weight before generic scale registration and skip the packed-data reduction on this path. This removes an unnecessary full-weight pass during low-memory export.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@modelopt/torch/export/unified_export_hf.py` around lines 668 - 679, Move the QTensorWrapper detection and retrieval of _scale/_double_scale in the export flow before get_weight_scaling_factor(...) and generic scale registration. For compressed weights, reuse the quantizer’s stored scales and skip the packed-data reduction entirely; preserve the existing generic calculation for uncompressed weights.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@modelopt/torch/export/unified_export_hf.py`:
- Around line 668-679: Move the QTensorWrapper detection and retrieval of
_scale/_double_scale in the export flow before get_weight_scaling_factor(...)
and generic scale registration. For compressed weights, reuse the quantizer’s
stored scales and skip the packed-data reduction entirely; preserve the existing
generic calculation for uncompressed weights.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 5f099001-1f77-4423-a9f0-9d7b51416b4e
📒 Files selected for processing (3)
CHANGELOG.rstmodelopt/torch/export/unified_export_hf.pytests/gpu/torch/export/test_export_weight_gpu.py
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2038 +/- ##
===========================================
+ Coverage 66.94% 77.93% +10.98%
===========================================
Files 519 519
Lines 59401 59419 +18
===========================================
+ Hits 39767 46308 +6541
+ Misses 19634 13111 -6523
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
b96737c to
f749100
Compare
| ) | ||
|
|
||
| if NVFP4QTensor._is_static_quantizer(weight_quantizer): | ||
| if ( |
There was a problem hiding this comment.
for readability: should we name this branch condition as real_nvfp4_quantized or something similar?
meenchen
left a comment
There was a problem hiding this comment.
Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.
The rescale approach is correct for the environment the author tested (no TensorRT-LLM in the process), and the new GPU test is a meaningful regression test. Three findings:
-
weight_quantizer._scaleis not always the modelopt per-block E4M3 layout.TensorQuantizer._real_quantize()callsNVFP4QTensor.quantize(..., try_tensorrt=True); whenfp4_compatible()(SM100+),block_size == 16, andtensorrt_llmis importable, it returns the cutlass-swizzled 1-D uint8 scale factor instead (seeNVFP4QTensor.dequantize, which explicitly detectsscale.dtype == torch.uint8 and scale.ndim == 1and converts viacutlass_fp4_scale_to_modelopt_fp4_scale). The new export branch has no such guard, so on Blackwell inside a TRT-LLM container--low_memory_modeNVFP4 export would write a 1-D scale built from raw uint8 byte values — the same class of bug this PR fixes, just in a different environment. The DGX Spark / vLLM validation wouldn't hit it, and the new test lives intests/gpu(nottests/gpu_trtllm), so CI won't either. Please either convert or raise loudly. -
Root cause of the
_double_scaleleak looks like a typo inRealQuantLinear.list_of_scale_tensors("double_scale"instead of"_double_scale").postprocess_state_dictalready stripsweight_quantizer._scalevia that list and would strip_double_scaletoo if the name matched. Worth confirming whether the one-word fix there is the better place (it also covers any other consumer of that list) rather than adding a second, export-path-only deletion mechanism. -
Minor: the new
.to(torch.float8_e4m3fn)skips the clamp that_cast_per_block_scale_to_fp8applies on every other NVFP4 path.
No licensing or design concerns; size and CHANGELOG are fine.
| # ``preprocess_linear_fusion`` unifies ``weight_scale_2`` over a fused group the ratio | ||
| # below is 1 only for the member owning the group max; the others take one extra E4M3 | ||
| # rounding (<= half-ULP, 6.25%). Avoiding that needs a shared scale at compress time. | ||
| weight_scale = ( |
There was a problem hiding this comment.
Bot comment.
_scale isn't guaranteed to be the modelopt per-block E4M3 tensor. TensorQuantizer._real_quantize() calls NVFP4QTensor.quantize(..., try_tensorrt=True), and when fp4_compatible() (device capability >= (10, 0)), block_size == 16, the weight is CUDA fp16/bf16, and tensorrt_llm imports, it returns the cutlass-format scale — a 1-D uint8 tensor in the 128x4 swizzled layout — and stores that in _scale. NVFP4QTensor.dequantize handles this case explicitly:
if kwarg["scale"].dtype == torch.uint8 and kwarg["scale"].ndim == 1:
kwarg["scale"] = cutlass_fp4_scale_to_modelopt_fp4_scale(kwarg["scale"], self.metadata["shape"][-2:])Here compressed_weight_scale.float() on that tensor yields raw byte values and a 1-D shape, so the exported weight_scale would again be wrong (wrong shape and meaningless values) — on Blackwell + TRT-LLM, which is a primary target for hf_ptq.py. The DGX Spark/vLLM validation and the new tests/gpu test both avoid this branch (no tensorrt_llm in-process), so neither catches it.
Suggest either converting via cutlass_fp4_scale_to_modelopt_fp4_scale before the rescale, or at minimum asserting compressed_weight_scale.dtype == torch.float8_e4m3fn and compressed_weight_scale.dim() == weight_scale_2.dim() + ... and raising a clear NotImplementedError, so the failure is loud rather than a silently malformed checkpoint.
| compressed_weight_scale.float() | ||
| * compressed_weight_scale_2.float().to(compressed_weight_scale.device) | ||
| / weight_scale_2.float().to(compressed_weight_scale.device) | ||
| ).to(torch.float8_e4m3fn) |
There was a problem hiding this comment.
Bot comment.
Every other NVFP4 path funnels the fp8 cast through _cast_per_block_scale_to_fp8 (in modelopt/torch/quantization/qtensor/nvfp4_tensor.py), which clamps to [2**-9, E4M3_MAX] specifically to avoid underflow→0 / overflow→NaN. Since weight_scale_2 here is the (possibly group-unified, i.e. >=) scale, the ratio is <= 1 and small block scales can underflow to 0 after the cast — a zero per-block scale silently zeroes that block at dequant. Reusing the existing helper (or at least clamping min=2**-9) would keep this consistent with the uncompressed path.
|
|
||
| # Internal to the quantizer: left registered they leak into the checkpoint as | ||
| # ``*.weight_quantizer._double_scale``, which downstream loaders reject with a KeyError. | ||
| if is_compressed_weight: |
There was a problem hiding this comment.
Bot comment.
postprocess_state_dict already has a mechanism for exactly this:
if any(key.endswith("weight_quantizer." + q_key) for q_key in RealQuantLinear.list_of_scale_tensors)and RealQuantLinear.list_of_scale_tensors = ["_scale", "double_scale", "_scale_zeros"] — note "double_scale" is missing its leading underscore, which is precisely why _scale never leaked but _double_scale did (matching the 560 stray *.weight_quantizer._double_scale keys and the absence of _scale keys in the bug report). Fixing that entry looks like the actual root cause and covers every consumer of that list, rather than adding a second deletion mechanism in this one export path. If you keep the delattr here (it does free memory earlier, which is a fair reason for --low_memory_mode), please still fix the list entry and say so in the CHANGELOG, otherwise the typo stays as a latent leak.
| weight_quantizer = getattr(compressed_module, quantizer_attrs.weight_quantizer) | ||
| assert getattr(weight_quantizer, "_scale", None) is None | ||
| assert getattr(weight_quantizer, "_double_scale", None) is None | ||
| assert not any("_double_scale" in key for key in compressed_module.state_dict()) |
There was a problem hiding this comment.
Bot comment.
This test only exercises the pure-PyTorch compression path. If tensorrt_llm is importable on a SM100+ runner, _real_quantize takes the torch.ops.trtllm.fp4_quantize branch and _scale becomes a 1-D uint8 cutlass scale — the assertions here would fail (or the export silently writes a bad scale in production). Consider adding coverage under tests/gpu_trtllm/ or explicitly monkeypatching fp4_compatible/the trtllm import to cover both layouts, so the behaviour is pinned in both environments.
mtq.compress (used by hf_ptq --low_memory_mode) replaces the weight with packed NVFP4 nibbles and stores the per-block scale on the quantizer. The NVFP4 export branch had no handling for this and recomputed the scale from the weight, which is now uint8 packed data: QTensorWrapper reports the packed shape, so the block count came out as in_dim/32 instead of in_dim/16 and the amax was taken over nibble-pair bytes. On DeepSeek-R1-Distill- Llama-70B this wrote weight_scale [8192, 256] instead of [8192, 512] with meaningless values, which dequantizes to garbage. The internal _scale / _double_scale buffers also stayed registered and were serialized as *.weight_quantizer._double_scale entries (560 of them in the 70B checkpoint), so vLLM and the TensorRT-LLM PyTorch backend failed with KeyError before loading any weight. Reuse the compression-time per-block scale, rescaled into the exported weight_scale_2 convention so the weight_scale * weight_scale_2 product that dequantization consumes is preserved, and drop the internal buffers after use. Verified on DGX Spark: the exported checkpoint now matches a normal export (identical key set, weight_scale_2 and input_scale identical, all 154 quantized tensors at the same dequantization error) and serves correctly instead of failing to load. NVBug 5987078 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
f749100 to
2314cb7
Compare
|
Thanks — all four addressed in 1. Swizzled cutlass scale — confirmed and fixed. You were right, and it reproduces on the hardware I was testing on. In a TRT-LLM container on GB10 (sm121): My earlier validation ran in the vLLM container, where 2. 3. Missing clamp — fixed. Now routed through 4. Redundant computation — fixed. Testing. New
|
meenchen
left a comment
There was a problem hiding this comment.
Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.
Re-review at 2314cb7f0: all four previously-raised items are addressed, and I found no new correctness problems.
Critical items — resolved
- Cutlass-swizzled
_scale(correctness, would have written raw byte values on Blackwell + TRT-LLM). New_compressed_per_block_scale()mirrors the guard inNVFP4QTensor.dequantize(1-D uint8 →cutlass_fp4_scale_to_modelopt_fp4_scale, hardImportErroriftensorrt_llmcan't be imported to convert). Verified the detection condition matches the producing branch inNVFP4QTensor.quantize(..., try_tensorrt=True). Newtests/gpu_trtllm/torch/export/test_export_compressed_nvfp4.pypins the swizzled layout and asserts the precondition so it can't silently degrade into the dense case. - Root cause of the
_double_scaleleak. Fixed at the source:RealQuantLinear.list_of_scale_tensors"double_scale"→"_double_scale", and the export-pathdelattrloop was dropped. I checked the other consumers of that list (megatron.py_parameter_to_keep_in_quantizer_state_dictusesk in key,_get_shard_axis_dictusesendswith("double_scale")) — both already matched._double_scalebefore, so the megatron path is unchanged, not a behavior change.postprocess_state_dict's exact"weight_quantizer." + q_keymatch is the only place the typo mattered, which is consistent with_scalebeing stripped and_double_scaleleaking in the bug report. The gpu test asserts both keys are gone. - Missing E4M3 clamp. Now routed through
_cast_per_block_scale_to_fp8, so the2**-9floor / 448 ceiling apply as on every other NVFP4 path — important here since group-unifiedweight_scale_2makes the rescale ratio ≤ 1 (underflow direction). - Redundant packed-data reduction.
get_weight_scaling_factor()is now skipped viaelif not use_compressed_scale, scoped to the NVFP4 family so other formats still register their scale. Confirmed the laterif weight_scale is not None: register_buffer(...)still writes the buffer, andto_quantized_weight()short-circuits onQTensorWrapper, so the packed nibbles are passed through untouched.
Also checked: BMM-style fused experts (gate_up_proj) are never compressed by pack_real_quantize_weight (no module.weight), so the transpose-orientation mismatch I was worried about for stored _scale can't be reached; the naming ask from cjluo-nv is covered by uses_compressed_nvfp4_scale / use_compressed_scale; the new file's header matches LICENSE_HEADER verbatim, so no licensing concern; size (+186/-3) and CHANGELOG are fine.
Minor, non-blocking: the CHANGELOG entry still describes the buffers as "removed after use" and doesn't mention the list_of_scale_tensors typo (the actual root cause) or the swizzled-scale conversion — worth a one-line touch-up on the way in. And if weight_scale_2 were ever None for an NVFP4 format, the compressed branch would fall through to recomputing from packed data (the original bug) rather than failing loudly; practically unreachable since weight_scale_2 is registered unconditionally for these formats.
Signed-off-by: Chenjie Luo <108829653+cjluo-nv@users.noreply.github.com>
|
#2041 #2031 #2061 #1983 #1628 (#2062) ## Cherry-picked PRs - #2022 - #2026 - #2032 - #1981 - #2010 - #2043 - #2042 - #2038 - #2050 - #2041 - #2031 - #2061 - #1983 - #1628 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added recipe-driven ONNX quantization and export for embedding and reranking models. * Added launcher support for inline commands, package requirements, and configurable Docker users. * Added QAD workflows, Nemotron data recipes, pruning, quantization, and evaluation examples. * Added evaluation accuracy and pruning score thresholds. * **Bug Fixes** * Improved ONNX control-flow conversion, compressed NVFP4 export, dataset loading, and multi-GPU DiffusionGemma handling. * **Documentation** * Updated CUDA 12/13 installation guidance and vLLM Blackwell deployment instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: vipandya <vipandya@nvidia.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com> Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com> Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> Signed-off-by: ajrasane <131806219+ajrasane@users.noreply.github.com> Signed-off-by: Meng Xin <mxin@nvidia.com> Signed-off-by: Chenjie Luo <chenjiel@nvidia.com> Signed-off-by: Chenjie Luo <108829653+cjluo-nv@users.noreply.github.com> Signed-off-by: Juhi Mittal <juhim@nvidia.com> Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com> Signed-off-by: Gwenaelle Cunha Sergio <gcunhasergio@nvidia.com> Co-authored-by: vishalpandya1990 <vishalpandya1990@gmail.com> Co-authored-by: Frida Hou <201670829+Fridah-nv@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Wei-Ming Chen <17592131+meenchen@users.noreply.github.com> Co-authored-by: Ajinkya Rasane <131806219+ajrasane@users.noreply.github.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: mxinO <164952785+mxinO@users.noreply.github.com> Co-authored-by: Chenjie Luo <108829653+cjluo-nv@users.noreply.github.com> Co-authored-by: Juhi Mittal <39641197+juhi10071998@users.noreply.github.com> Co-authored-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com>
What does this PR do?
Type of change: Bug fix
Fixes unified HF export of already-compressed NVFP4 weights —
mtq.compressand, through it,examples/llm_ptq/hf_ptq.py --low_memory_mode(NVBug 5987078). Two defects, one root cause: the NVFP4 export branch has no handling for weights that were already real-quantized, unlike theFP8_PB_REALbranch which consumesweight_quantizer._scale.1.
weight_scalewas recomputed from packed data. After compression the weight is aQTensorWrapperof packed NVFP4 nibbles, andQTensorWrapper.__new__builds the Parameter from_quantized_data, so.shapereports the packed shape (the logical shape survives only inmetadata["shape"]). The export derived the block count fromweight.shape[-1]and took amax over nibble-pair bytes, so it wrote a scale of half the required size with meaningless values:weightweight_scalewrittenq_proj[2048, 1024]U8[2048, 64][2048, 128]q_proj[8192, 4096]U8[8192, 256][8192, 512]2. An internal quantizer buffer leaked into the checkpoint.
postprocess_state_dictstripsweight_quantizer.<name>for every name inRealQuantLinear.list_of_scale_tensors, but that list carried"double_scale"where the buffer is_double_scale— a missing underscore. So_scalewas stripped and_double_scalewas not, and it reached the checkpoint as*.weight_quantizer._double_scale(560 entries in the 70B checkpoint). Downstream loaders reject it before loading any weight:This is why only the TensorRT backend appeared usable in the bug report — its converter tolerates the stray key, then produces
!!!!!!output from the broken scales, while the PyTorch backend (vLLM / TensorRT-LLM) fails to load outright.The fix reuses the per-block scale captured at compression time, rescaled into the exported
weight_scale_2convention, and corrects the typo above. The rescale matters: compression normalizes per-block FP8 scales against the global scale it captured at that moment, which is not the post-calibrationweight_scale_2the export writes. Exporting the stored scale as-is loads fine but leaves every block off by a constant factor (~1.96x measured), so theweight_scale * weight_scale_2product that dequantization consumes must be preserved.Note the typo fix also affects
modelopt/torch/quantization/plugins/megatron.py:505,511, which filter on the same list — these are internal buffers so excluding them looks correct, but calling it out since it is a behavior change outside the export path.Compression-time scale layout.
TensorQuantizer._real_quantizecallsNVFP4QTensor.quantize(..., try_tensorrt=True), so on an FP4-capable device with TensorRT-LLM importable the stored_scaleis the cutlass-swizzled 1-D uint8 scale rather than the modelopt 2-D E4M3 layout. Confirmed on GB10 in a TRT-LLM container:The export therefore normalizes it the same way
NVFP4QTensor.dequantizedoes, and raises iftensorrt_llmcannot be imported to convert, rather than writing raw byte values.Usage
No API change. The previously broken path now works:
Testing
All runs on DGX Spark (GB10, sm121, aarch64). Both compression-time scale layouts are covered, since the layout depends on whether TensorRT-LLM is importable in the process:
New tests
tests/gpu/torch/export/test_export_weight_gpu.py::test_export_compressed_nvfp4_weight— dense E4M3 path. Asserts the per-block scale covers the logical input dim, thatweight_scale * weight_scale_2matches an uncompressed export of the same model, and thatpostprocess_state_dictstrips both internal buffers.tests/gpu_trtllm/torch/export/test_export_compressed_nvfp4.py::test_export_compressed_nvfp4_weight_trtllm_scale— cutlass-swizzled path. Asserts as a precondition that the environment really produced a 1-D uint8 scale, so it cannot silently degrade into the dense case when TensorRT-LLM is absent.Results
Negative controls (each test fails without the code it guards)
mainwith only the test file applied:test_export_compressed_nvfp4_weightfails.test_export_compressed_nvfp4_weight_trtllm_scalefails.End-to-end, TensorRT-LLM container (the environment from the bug report, where
_scaleis swizzled) — TinyLlama-1.1B,--qformat nvfp4 --low_memory_mode, plus a normal export as control. Exported checkpoints are structurally identical (0 stray_double_scalekeys vs. 560 onmain; 663 keys each;q_proj.weight_scale[2048, 128]FP8 in both; QKVweight_scale_2unified in both). Loaded on the TensorRT-LLM PyTorch backend — the backend reported as unusable:On
mainthat same load fails withKeyError: '...weight_quantizer._double_scale'before a single weight is read.End-to-end, vLLM container (dense scale path) — TinyLlama-1.1B, NVFP4 +
--low_memory_mode:KeyError: '...weight_quantizer._double_scale', engine fails to start"Paris is the capital of"→" France and is best known for the awe inspiring Notre-Dame C")input_scaleidentical, andweight_scale_2identical — including the values unified across fused groups, so the fused-GEMM contract (one sharedweight_scale_2per QKV / gate-up group) still holds.mainand is the source of the numbers in the table above.Known limitation: fused groups are slightly less accurate than a full-memory PTQ
This restores a working checkpoint, but
--low_memory_modeNVFP4 is not numerically identical to a normal PTQ, and cannot be made so at export time.The nibbles are packed at load time against the layer's own global scale, so the effective per-block scale baked into them is
fp8_own * ws2_own.preprocess_linear_fusionlater unifiesweight_scale_2across a fused group to the group max, and the format requires the per-block scale be E4M3, so the best the export can write isround_fp8(fp8_own * ws2_own / ws2_unified). For the group member owning the max amax that ratio is exactly 1 and the round trip is bit-exact; for the others it costs one extra E4M3 rounding, bounded by a half-ULP (6.25%). The uncompressed path never pays this because its weights are still high precision at export, soto_quantized_weightre-quantizes the nibbles after unification.Measured on TinyLlama-1.1B (weight relative error vs. the source BF16 weights, 154 quantized tensors):
--low_memory_mode(this PR)The degradation is confined to exactly 66 of 154 tensors = 22 layers x 3, i.e. the non-max members of each
q/k/vandgate/upgroup (worst observed +0.0066, e.g.layers.11.self_attn.k_proj0.0898 -> 0.0964). The 88 remaining tensors — group winners plus the unfusedo_proj/down_proj— are bit-exact.Removing this requires compressing a fusion group against one shared scale in the compress-on-load path (
RealQuantParameterDict), where the group max first becomes known; that is a larger change and is left as a follow-up.Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
NVBug 5987078.
Not addressed here: at 70B scale on DGX Spark,
--low_memory_modecan also crash before export when the device map offloads, becauseQTensorWrapper.to()cannot represent ametatensor:It is reproducible on demand under low free GPU memory (crashes at 41.2 GB and 58.5 GB free; succeeds at 88.1 GB) and is easy to hit on Spark's unified memory, where page cache from reading the checkpoint counts against
torch.cuda.mem_get_info(). That is an independent defect and will be filed separately.Summary by CodeRabbit
Bug Fixes
Tests