66
77
88import contextlib
9+ import copy
10+ import ctypes
11+ import functools
12+ import gc
913import logging
1014import os
1115import shutil
2529from executorch .backends .cuda .triton .replacement_pass import (
2630 ReplaceEdgeOpWithTritonOpPass ,
2731)
32+ from executorch .exir ._serialize ._cord import FileBackedData
2833from executorch .exir ._warnings import experimental
2934from executorch .exir .backend .backend_details import BackendDetails
3035from executorch .exir .backend .compile_spec_schema import CompileSpec
@@ -60,6 +65,14 @@ def _is_cpu_clone_active() -> bool:
6065 return getattr (_CPU_CLONE_GUARD , "active" , False )
6166
6267
68+ def _trim_host_memory () -> None :
69+ gc .collect ()
70+ try :
71+ ctypes .CDLL (None ).malloc_trim (0 )
72+ except AttributeError :
73+ pass
74+
75+
6376def _full_zeros_preserving_strides (x : torch .Tensor , device ) -> torch .Tensor :
6477 """Allocate a zero-filled tensor matching ``x``'s size/stride/dtype on ``device``.
6578
@@ -82,18 +95,29 @@ def _is_emptied(x) -> bool:
8295 )
8396
8497
98+ def _tensor_properties_for_low_memory (tensor , original ):
99+ if _is_emptied (tensor ):
100+ return None
101+ return original (tensor )
102+
103+
85104@contextlib .contextmanager
86105def _compile_time_cpu_clones (target_device : torch .device ):
87106 """Force AOTI's mutated-buffer clones onto CPU while preserving the
88107 serialized constants' target device."""
89- from torch ._inductor import compile_fx as _cfx , graph as _graph
108+ from torch ._inductor import (
109+ codecache as _codecache ,
110+ compile_fx as _cfx ,
111+ graph as _graph ,
112+ )
90113 from torch ._inductor .codegen .cpp_wrapper_cpu import CppWrapperCpu as _Cpp
91114 from torch ._inductor .graph import GraphLowering as _GL
92115
93116 orig_clone = _cfx .clone_preserve_strides
94117 orig_codegen_device = _Cpp .codegen_device
95118 orig_get_const = _GL .get_original_value_of_constant
96119 orig_is_same = _graph .is_same_tensor
120+ orig_tensor_properties = _codecache .TensorProperties
97121
98122 def _is_same_skip_emptied (data , value ):
99123 # KV buffers freed via resize_(0) all have data_ptr 0, so the stock
@@ -152,6 +176,9 @@ def _codegen_device_target_aware(self, device):
152176 _Cpp .codegen_device = _codegen_device_target_aware
153177 _GL .get_original_value_of_constant = _get_const_synthesize_zeros
154178 _graph .is_same_tensor = _is_same_skip_emptied
179+ _codecache .TensorProperties = functools .partial (
180+ _tensor_properties_for_low_memory , original = orig_tensor_properties
181+ )
155182 prev_active = getattr (_CPU_CLONE_GUARD , "active" , False )
156183 _CPU_CLONE_GUARD .active = True
157184 try :
@@ -162,6 +189,7 @@ def _codegen_device_target_aware(self, device):
162189 _Cpp .codegen_device = orig_codegen_device
163190 _GL .get_original_value_of_constant = orig_get_const
164191 _graph .is_same_tensor = orig_is_same
192+ _codecache .TensorProperties = orig_tensor_properties
165193
166194
167195def _is_kv_buffer (name , v ) -> bool :
@@ -270,6 +298,38 @@ def _on_off_compile_spec_value(spec: CompileSpec) -> bool:
270298 return value == "ON"
271299
272300
301+ def _write_aoti_weights_blob (weights , blob_path : str ) -> None :
302+ _trim_host_memory ()
303+ tensors = [tensor for tensor , _ in weights .values ()]
304+ all_cuda = all (tensor .is_cuda for tensor in tensors )
305+ chunk_size = 8 * 1024 * 1024
306+
307+ with open (blob_path , "wb" ) as output :
308+ for tensor in tensors :
309+ if tensor .is_mkldnn :
310+ raise RuntimeError ("MKLDNN constants are not supported by CUDA AOTI" )
311+ storage = tensor .untyped_storage ()
312+ nbytes = storage .nbytes ()
313+ if nbytes and tensor .is_cuda :
314+ byte_tensor = torch .empty (
315+ 0 , dtype = torch .uint8 , device = tensor .device
316+ ).set_ (storage , 0 , (nbytes ,), (1 ,))
317+ for offset in range (0 , nbytes , chunk_size ):
318+ cpu_chunk = byte_tensor [offset : offset + chunk_size ].cpu ()
319+ output .write (memoryview (cpu_chunk .numpy ()))
320+ del byte_tensor , cpu_chunk
321+ elif nbytes :
322+ raw_array = (ctypes .c_ubyte * nbytes ).from_address (storage .data_ptr ())
323+ raw_view = memoryview (raw_array ).cast ("B" )
324+ for offset in range (0 , nbytes , chunk_size ):
325+ output .write (raw_view [offset : offset + chunk_size ])
326+ del raw_view , raw_array
327+ if not all_cuda and (padding := (- nbytes ) % 64 ):
328+ output .write (bytes (padding ))
329+ del storage
330+ _trim_host_memory ()
331+
332+
273333@final
274334@experimental (
275335 "This API and all of cuda backend related functionality are experimental."
@@ -384,6 +444,56 @@ def save_data_externally(cls) -> bool:
384444 """
385445 return True
386446
447+ @classmethod
448+ def load_weights_blob (
449+ cls , blob_path : str , compile_specs : List [CompileSpec ]
450+ ) -> tuple [Any , str ]:
451+ if not cls ._is_low_memory_mode (compile_specs ):
452+ return super ().load_weights_blob (blob_path , compile_specs )
453+ blob_data = FileBackedData .move_from (blob_path )
454+ return blob_data , blob_data .sha256 ().hex ()
455+
456+ @classmethod
457+ def materialize_weights_blob (
458+ cls , paths : Any , compile_specs : List [CompileSpec ]
459+ ) -> Any :
460+ if not cls ._is_low_memory_mode (compile_specs ) or not isinstance (paths , list ):
461+ return paths
462+
463+ from torch .export .pt2_archive ._package_weights import Weights
464+
465+ weights = [path for path in paths if isinstance (path , Weights )]
466+ if not weights :
467+ return paths
468+ if len (weights ) != 1 :
469+ raise RuntimeError (
470+ f"Expected one CUDA AOTI weights output, got { len (weights )} "
471+ )
472+
473+ so_path = next (
474+ path
475+ for path in paths
476+ if isinstance (path , str ) and path .endswith (".wrapper.so" )
477+ )
478+ blob_path = os .path .splitext (so_path )[0 ] + "_weights.blob"
479+ _write_aoti_weights_blob (weights [0 ], blob_path )
480+ return [path for path in paths if not isinstance (path , Weights )] + [blob_path ]
481+
482+ @classmethod
483+ def copy_exported_program_for_preprocess (
484+ cls , edge_program , compile_specs : List [CompileSpec ]
485+ ):
486+ if not cls ._is_low_memory_mode (compile_specs ):
487+ return copy .deepcopy (edge_program )
488+
489+ tensor_memo = {
490+ id (tensor ): tensor
491+ for values in (edge_program .state_dict , edge_program .constants )
492+ for tensor in values .values ()
493+ if isinstance (tensor , torch .Tensor )
494+ }
495+ return copy .deepcopy (edge_program , tensor_memo )
496+
387497 @classmethod
388498 def get_supported_fallback_kernels (cls ) -> Dict [str , Any ]:
389499 return {
@@ -459,7 +569,9 @@ def get_aoti_compile_options(
459569 "aot_inductor.package" : True ,
460570 "aot_inductor.package_constants_in_so" : False ,
461571 # Store weight constants on disk in a binary blob
462- "aot_inductor.package_constants_on_disk_format" : "binary_blob" ,
572+ "aot_inductor.package_constants_on_disk_format" : cls ._weights_format (
573+ compile_specs
574+ ),
463575 # Enable maximum automatic tuning for optimal performance
464576 "max_autotune" : True ,
465577 # Use TRITON for GEMM (General Matrix Multiply) operations tuning only to avoid using operators in libtorch
@@ -594,6 +706,7 @@ def _combined():
594706 stack .enter_context (
595707 _compile_time_cpu_clones (torch .device (cls .get_device_name ()))
596708 )
709+ _trim_host_memory ()
597710 yield
598711
599712 return _combined ()
@@ -606,6 +719,14 @@ def _is_low_memory_mode(compile_specs: List[CompileSpec]) -> bool:
606719 return spec .value .decode ("utf-8" ).upper () == "ON"
607720 return False
608721
722+ @classmethod
723+ def _weights_format (cls , compile_specs : List [CompileSpec ]) -> str :
724+ return (
725+ "pickle_weights"
726+ if cls ._is_low_memory_mode (compile_specs )
727+ else "binary_blob"
728+ )
729+
609730 @classmethod
610731 def move_program_to_device (
611732 cls ,
0 commit comments