@@ -24,6 +24,12 @@ type GoMLXModel struct {
2424 Exec * context.Exec // exec is used to execute the model with a context.
2525 Call func (ctx * context.Context , inputs []* graph.Node ) []* graph.Node
2626 Destroy func ()
27+ // BatchBuckets defines bucket sizes for batch dimension padding.
28+ // Coarse bucketing reduces JIT cache pressure by limiting unique shapes.
29+ BatchBuckets []int
30+ // SequenceBuckets defines bucket sizes for sequence length padding.
31+ // Coarse bucketing reduces JIT cache pressure by limiting unique shapes.
32+ SequenceBuckets []int
2733}
2834
2935func loadExternalData (path string , model * onnx.Model ) error {
@@ -131,14 +137,44 @@ func createGoMLXModelBackend(model *Model, options *options.Options) error {
131137 return contextErr
132138 }
133139
134- exec .SetMaxCache (- 1 )
140+ // Limit JIT compilation cache to bound memory growth.
141+ //
142+ // Each unique input shape triggers XLA JIT recompilation, and each compiled
143+ // program consumes 50-200MB depending on model complexity. With unlimited
144+ // cache (-1), memory can grow unbounded as different request shapes accumulate.
145+ //
146+ // IMPORTANT: The cache is NOT LRU - if full, new shapes return errors.
147+ // The cache size must be >= the number of unique shapes from bucketing:
148+ // - Default batch buckets: 1, 8, 32 (3 values)
149+ // - Default sequence buckets: 32, 128, 512 (3 values)
150+ // - Maximum unique shapes: 3 × 3 = 9 per model
151+ //
152+ // Default cache of 16 provides headroom above the 9 possible shapes.
153+ // Configure via WithGoMLXMaxCache() if using custom bucket configurations.
154+ maxCache := 16
155+ if options .GoMLXOptions .MaxCache > 0 {
156+ maxCache = options .GoMLXOptions .MaxCache
157+ }
158+ exec .SetMaxCache (maxCache )
159+
160+ // Use configured buckets or fall back to defaults.
161+ batchBucketsCfg := batchBuckets
162+ if len (options .GoMLXOptions .BatchBuckets ) > 0 {
163+ batchBucketsCfg = options .GoMLXOptions .BatchBuckets
164+ }
165+ sequenceBucketsCfg := sequenceBuckets
166+ if len (options .GoMLXOptions .SequenceBuckets ) > 0 {
167+ sequenceBucketsCfg = options .GoMLXOptions .SequenceBuckets
168+ }
135169
136170 model .GoMLXModel = & GoMLXModel {
137- Backend : backend ,
138- OnnxModel : modelParsed ,
139- Ctx : ctx ,
140- Exec : exec ,
141- Call : callFunc ,
171+ Backend : backend ,
172+ OnnxModel : modelParsed ,
173+ Ctx : ctx ,
174+ Exec : exec ,
175+ Call : callFunc ,
176+ BatchBuckets : batchBucketsCfg ,
177+ SequenceBuckets : sequenceBucketsCfg ,
142178 Destroy : func () {
143179 exec .Finalize ()
144180 ctx .Finalize ()
@@ -188,11 +224,15 @@ func createInputTensorsGoMLX(batch *PipelineBatch, model *Model, padBatchDimensi
188224
189225 batchSize := batch .Size
190226 if padBatchDimension {
191- batchSize = nextPowerOf2 (batchSize )
227+ // Use coarse bucketing instead of power-of-2 to reduce JIT cache pressure.
228+ // See shapeBucket() for memory savings analysis.
229+ batchSize = shapeBucket (batchSize , model .GoMLXModel .BatchBuckets )
192230 }
193231 maxSeqLength := batch .MaxSequenceLength
194232 if padSequenceDimension && ! leftPad {
195- maxSeqLength = nextPowerOf2 (maxSeqLength )
233+ // Use coarse bucketing instead of power-of-2 to reduce JIT cache pressure.
234+ // See shapeBucket() for memory savings analysis.
235+ maxSeqLength = shapeBucket (maxSeqLength , model .GoMLXModel .SequenceBuckets )
196236 }
197237 total := batchSize * maxSeqLength
198238
@@ -324,21 +364,21 @@ func runGoMLXSessionOnBatch(batch *PipelineBatch, p *BasePipeline) error {
324364 if err != nil {
325365 return err
326366 }
327- defer func () {
328- for _ , t := range outputTensors {
329- err = errors .Join (t .FinalizeAll ())
330- }
331- }()
332367
333368 convertedOutput := make ([]any , len (outputTensors ))
334369 for i , t := range outputTensors {
335370 var rawOutput []float32
371+ // ConstFlatData automatically triggers MaterializeLocal under the hood,
372+ // copying data from device (TPU/GPU) to local (CPU) memory.
336373 err = tensors .ConstFlatData (t , func (flat []float32 ) {
337374 rawOutput = flat
338375 })
339376 if err != nil {
340377 return err
341378 }
379+ // FinalizeAll immediately frees both the device copy and the local copy.
380+ // This is critical for TPU/GPU where Go's GC doesn't see device-side allocations.
381+ _ = t .FinalizeAll ()
342382 convertedOutput [i ] = ReshapeOutput (rawOutput , p .Model .OutputsMeta [i ], batch .Size , batch .PaddingMask , batch .MaxSequenceLength )
343383 }
344384
@@ -366,6 +406,41 @@ func nextPowerOf2(n int) int {
366406 return pow
367407}
368408
409+ // shapeBucket quantizes input dimensions to coarse buckets to reduce JIT cache pressure.
410+ //
411+ // XLA compiles a separate program for each unique input shape. Using power-of-2 padding
412+ // creates up to 42 unique shapes per model (6 batch sizes × 7 sequence lengths), which
413+ // can consume 2-8GB of memory for cached compiled programs.
414+ //
415+ // Coarse bucketing reduces this to just 9 shapes (3 batch × 3 sequence), significantly
416+ // reducing memory usage while maintaining reasonable padding overhead:
417+ //
418+ // Batch buckets: 1, 8, 32 (covers 1-32 batch sizes)
419+ // Sequence buckets: 32, 128, 512 (covers 1-512 token sequences)
420+ //
421+ // Trade-off: Slightly more padding waste for small inputs (e.g., batch=2 pads to 8),
422+ // but dramatically fewer compiled programs in memory.
423+ //
424+ // Example memory savings with 2 models (embedder + reranker):
425+ // - Power-of-2: 42 shapes × 2 models × 100MB avg = 8.4GB
426+ // - Coarse buckets: 9 shapes × 2 models × 100MB avg = 1.8GB
427+ func shapeBucket (n int , buckets []int ) int {
428+ for _ , bucket := range buckets {
429+ if n <= bucket {
430+ return bucket
431+ }
432+ }
433+ // Return the largest bucket if n exceeds all
434+ return buckets [len (buckets )- 1 ]
435+ }
436+
437+ // Default shape buckets for batch size and sequence length.
438+ // These provide a good balance between padding overhead and JIT cache size.
439+ var (
440+ batchBuckets = []int {1 , 8 , 32 }
441+ sequenceBuckets = []int {32 , 128 , 512 }
442+ )
443+
369444func (goMLXModel * GoMLXModel ) Save (w io.Writer ) error {
370445 if err := goMLXModel .OnnxModel .ContextToONNX (goMLXModel .Ctx ); err != nil {
371446 return err
0 commit comments