4444//
4545// # Options
4646//
47- // Those can be passed after the plugin name, e.g.: GOMLX_BACKEND="xla:my_pjrt,notf32,shared_buffers ".
47+ // Those can be passed after the plugin name, e.g.: GOMLX_BACKEND="xla:cuda,tf32=false,preallocate=false ".
4848//
49- // - "tf32", "notf32" : controls whether to use TF32 for DotGeneral operations that are using float32
49+ // - "tf32" (boolean, default=true) : controls whether to use TF32 for DotGeneral operations that are using float32
5050// (it can be faster in modern GPUs). It's enabled by default.
51- // - "shared_buffers", "noshared_buffers" : controls whether to use shared buffers for the device buffer
51+ // - "shared_buffer" (boolean, default=true) : controls whether to use shared buffers for the device buffer
5252// (where device=CPU). It's enabled by default if the plugin is called "cpu".
53+ // - "preallocate" (boolean, default=true): whether the CUDA PJRT preallocates a large portion of the memory.
54+ // - "memory_fraction" (float, default=0.75): how much memory to preallocate.
55+ // - "allocator" (string, default="default"): which allocator to use. For CUDA the available ones are "default"
56+ // (== "bfc"), "bfc" ("best-fit for coalescing", avoids framementation), "cuda_async" (dynamic, no preallocation),
57+ // "platform" (slow, good for debugging), "vmm"
58+ // - "visible_devices" (list of integers, e.g., "0;1;2"): list IDs of the devices made visible to the backend.
59+ // - "use_tfrt_gpu_client" (boolean, default=false): uses the "TFRT" dispatcher for GPU.
5360//
5461// # (NO) Dynamic Shapes
5562//
@@ -64,6 +71,7 @@ import (
6471 "os"
6572 "path/filepath"
6673 "slices"
74+ "strconv"
6775 "strings"
6876
6977 "github.com/gomlx/compute"
@@ -107,20 +115,59 @@ func New(config string) (compute.Backend, error) {
107115 return NewWithOptions (config , nil )
108116}
109117
118+ // optionsDocumentation contains the help text for the options.
119+ const optionsDocumentation = `"xla" backend extra options:
120+ - "tf32" (boolean, default=true): controls whether to use TF32 for DotGeneral operations that are using float32
121+ (it can be faster in modern GPUs). It's enabled by default.
122+ - "shared_buffer" (boolean, default=true): controls whether to use shared buffers for the device buffer
123+ (where device=CPU). It's enabled by default if the plugin is called "cpu".
124+ - "preallocate" (boolean, default=true): whether the CUDA PJRT preallocates a large portion of the memory.
125+ - "memory_fraction" (float, default=0.75): how much memory to preallocate.
126+ - "allocator" (string, default="default"): which allocator to use. For CUDA the available ones are "default"
127+ (== "bfc"), "bfc" ("best-fit for coalescing", avoids framementation), "cuda_async" (dynamic, no preallocation),
128+ "platform" (slow, good for debugging), "vmm"
129+ - "visible_devices" (list of integers, e.g., "0;1;2"): list IDs of the devices made visible to the backend.
130+ - "use_tfrt_gpu_client" (boolean, default=false): uses the "TFRT" dispatcher for GPU.`
131+
110132// NewWithOptions creates a StableHLO backend with the given client options.
111133// It allows more control, not available with the default New constructor.
112134//
113135// This function triggers AutoInstall if it is enabled (the default). See EnableAutoInstall to disable it.
114136func NewWithOptions (config string , options pjrt.NamedValuesMap ) (* Backend , error ) {
115137 pluginName := config
116- var pluginOptions []string
138+
139+ // Make shallow copy options, since we may change it:
140+ pluginOptions := make (pjrt.NamedValuesMap )
141+ for key , value := range options {
142+ pluginOptions [key ] = value
143+ }
144+
145+ // Parse backendOptions from config string.
146+ backendOptions := make (map [string ]string )
117147 parts := strings .Split (config , "," )
118148 if len (parts ) > 1 {
119- // Plugin options (exclude empty).
120- pluginOptions = slices .DeleteFunc (parts [1 :], func (s string ) bool { return s == "" })
121149 pluginName = parts [0 ]
150+ for _ , part := range parts [1 :] {
151+ if part == "" {
152+ continue
153+ }
154+ key , val , found := strings .Cut (part , "=" )
155+ if ! found {
156+ backendOptions [part ] = ""
157+ } else {
158+ backendOptions [key ] = val
159+ }
160+ }
122161 }
123162
163+ _ , helpOptionSet := backendOptions ["help" ]
164+ if pluginName == "help" || helpOptionSet {
165+ klog .Infof ("Available plugins: %q" , GetAvailablePlugins ())
166+ klog .Info (optionsDocumentation )
167+ return nil , errors .New ("Help requested" )
168+ }
169+
170+ // FInd plugin.
124171 if ! filepath .IsAbs (pluginName ) {
125172 if autoInstall {
126173 err := AutoInstall ()
@@ -148,54 +195,156 @@ func NewWithOptions(config string, options pjrt.NamedValuesMap) (*Backend, error
148195 }
149196 }
150197
151- plugin , err := pjrt .GetPlugin (pluginName )
152- if err != nil {
153- return nil , errors .WithMessagef (err , "backend %q:" , BackendName )
154- }
155- var client * pjrt.Client
156- client , err = plugin .NewClient (options )
157- if err != nil {
158- return nil , errors .WithMessagef (err , "while creating plugin %s for backend %q" , pluginName , BackendName )
159- }
160- klog .V (1 ).Infof ("created new plugin %q for backend %q" , pluginName , BackendName )
198+ // Create backend option (not associated with a plugin yet)
161199 backend := & Backend {
162- plugin : plugin ,
163- client : client ,
164200 pluginName : pluginName ,
165201 config : config ,
166202 capabilities : Capabilities .Clone (),
167- numDevices : len (client .AddressableDevices ()),
168203
169204 // Enable TF32 by default for CUDA.
170205 DotGeneralUseTF32 : isPluginType (pluginName , "cuda" ),
206+
207+ // SharedBuffers is true for CPU by default
208+ hasSharedBuffers : isPluginType (pluginName , "cpu" ),
171209 }
172210
173211 // Support "shared buffers":
174- backend .hasSharedBuffers = isPluginType (pluginName , "cpu" )
175- if idx := slices .Index (pluginOptions , "shared_buffers" ); idx != - 1 {
176- backend .hasSharedBuffers = true
177- pluginOptions = slices .Delete (pluginOptions , idx , idx + 1 )
178- } else if idx := slices .Index (pluginOptions , "noshared_buffers" ); idx != - 1 {
179- backend .hasSharedBuffers = false
180- pluginOptions = slices .Delete (pluginOptions , idx , idx + 1 )
212+ if b , found , err := parseOptions [bool ]("shared_buffers" , backendOptions ); err != nil {
213+ return nil , err
214+ } else if found {
215+ backend .hasSharedBuffers = b
181216 }
182217
183218 // Support for tf32 DotGeneral.
184- if idx := slices .Index (pluginOptions , "tf32" ); idx != - 1 {
185- backend .DotGeneralUseTF32 = true
186- pluginOptions = slices .Delete (pluginOptions , idx , idx + 1 )
187- } else if idx := slices .Index (pluginOptions , "notf32" ); idx != - 1 {
188- backend .DotGeneralUseTF32 = false
189- pluginOptions = slices .Delete (pluginOptions , idx , idx + 1 )
219+ if b , found , err := parseOptions [bool ]("tf32" , backendOptions ); err != nil {
220+ return nil , err
221+ } else if found {
222+ backend .DotGeneralUseTF32 = b
223+ }
224+
225+ // Support "preallocate":
226+ if b , found , err := parseOptions [bool ]("preallocate" , backendOptions ); err != nil {
227+ return nil , err
228+ } else if found {
229+ pluginOptions ["preallocate" ] = b
230+ }
231+
232+ // Control memory fraction of preallocated memory:
233+ if f , found , err := parseOptions [float32 ]("memory_fraction" , backendOptions ); err != nil {
234+ return nil , err
235+ } else if found {
236+ pluginOptions ["memory_fraction" ] = f
237+ }
238+
239+ // Allocator to use for CUDA:
240+ if s , found , err := parseOptions [string ]("allocator" , backendOptions ); err != nil {
241+ return nil , err
242+ } else if found {
243+ pluginOptions ["allocator" ] = s
244+ }
245+
246+ // Visible devices for the client:
247+ if visibleDevices , found , err := parseOptions [[]int64 ]("visible_devices" , backendOptions ); err != nil {
248+ return nil , err
249+ } else if found {
250+ pluginOptions ["visible_devices" ] = visibleDevices
251+ }
252+
253+ // Use TFRT GPU client:
254+ if useTFRT , found , err := parseOptions [bool ]("use_tfrt_gpu_client" , backendOptions ); err != nil {
255+ return nil , err
256+ } else if found {
257+ pluginOptions ["use_tfrt_gpu_client" ] = useTFRT
190258 }
191259
192260 // Any leftover plugin options are unknown.
193- if len (pluginOptions ) != 0 {
194- klog .Errorf ("backend %q: unknown plugin options %q " , BackendName , pluginOptions )
261+ if len (backendOptions ) != 0 {
262+ klog .Errorf ("backend %q: unknown plugin options %v " , BackendName , xslices . SortedKeys ( backendOptions ) )
195263 }
264+
265+ // Create plugin.
266+ plugin , err := pjrt .GetPlugin (pluginName )
267+ if err != nil {
268+ return nil , errors .WithMessagef (err , "backend %q:" , BackendName )
269+ }
270+ var client * pjrt.Client
271+ client , err = plugin .NewClient (pluginOptions )
272+ if err != nil {
273+ return nil , errors .WithMessagef (err , "while creating plugin %s for backend %q" , pluginName , BackendName )
274+ }
275+ klog .V (1 ).Infof ("created new plugin %q for backend %q" , pluginName , BackendName )
276+
277+ // Set backend.
278+ backend .plugin = plugin
279+ backend .client = client
280+ backend .numDevices = len (client .AddressableDevices ())
281+
196282 return backend , nil
197283}
198284
285+ // parseOptions parses the optionName from backendOptions (string).
286+ // If optionName is found, it's removed from backendOptions.
287+ // For bool options, it also searches for "no"+optionName, and if found, removes it and returns false.
288+ // It returns the parsed value, whether it was found, and any parsing error.
289+ func parseOptions [T interface {
290+ string | bool | float32 | []int64
291+ }](
292+ optionName string , backendOptions map [string ]string ) (T , bool , error ) {
293+ var val T
294+
295+ if _ , ok := any (val ).(bool ); ok {
296+ noKey := "no" + optionName
297+ if _ , foundNo := backendOptions [noKey ]; foundNo {
298+ delete (backendOptions , noKey )
299+ return any (false ).(T ), true , nil
300+ }
301+ }
302+
303+ valStr , found := backendOptions [optionName ]
304+ if ! found {
305+ return val , false , nil
306+ }
307+ delete (backendOptions , optionName )
308+
309+ switch any (val ).(type ) {
310+ case string :
311+ return any (valStr ).(T ), true , nil
312+ case bool :
313+ if valStr == "" {
314+ return any (true ).(T ), true , nil
315+ }
316+ b , err := strconv .ParseBool (valStr )
317+ if err != nil {
318+ return val , true , errors .Wrapf (err , "Failed to parse option %q=%q" , optionName , valStr )
319+ }
320+ return any (b ).(T ), true , nil
321+ case float32 :
322+ f , err := strconv .ParseFloat (valStr , 32 )
323+ if err != nil {
324+ return val , true , errors .Wrapf (err , "Failed to parse option %q=%q" , optionName , valStr )
325+ }
326+ return any (float32 (f )).(T ), true , nil
327+ case []int64 :
328+ if valStr == "" {
329+ return val , true , nil
330+ }
331+ parts := strings .FieldsFunc (valStr , func (r rune ) bool {
332+ return r == ';' || r == ':' || r == ' '
333+ })
334+ res := make ([]int64 , 0 , len (parts ))
335+ for _ , part := range parts {
336+ valInt , err := strconv .ParseInt (part , 10 , 64 )
337+ if err != nil {
338+ return val , true , errors .Wrapf (err , "Failed to parse option %q=%q" , optionName , valStr )
339+ }
340+ res = append (res , valInt )
341+ }
342+ return any (res ).(T ), true , nil
343+ default :
344+ panic ("unreachable" )
345+ }
346+ }
347+
199348// Registers New() as the default constructor for "xla" backend.
200349func init () {
201350 compute .Register (BackendName , New )
0 commit comments