Skip to content

Commit 4811968

Browse files
committed
Added parseOptions to faciliate parsing of options.
Added and documented various XLA options made available by `GOMLX_BACKEND` (or the config string passed). Added `help` plugin/option: which fails to create the XLA backend, but prints out documentation on available plugins and options.
1 parent 368face commit 4811968

3 files changed

Lines changed: 243 additions & 44 deletions

File tree

compute/xla/parse_options_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2026 The GoMLX Authors. SPDX-License-Identifier: Apache-2.0
2+
3+
package xla
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestParseOptions(t *testing.T) {
12+
// Test bool option
13+
opts := map[string]string{
14+
"foo": "true",
15+
"bar": "false",
16+
"baz": "",
17+
"nofizz": "",
18+
}
19+
20+
val, found, err := parseOptions[bool]("foo", opts)
21+
assert.NoError(t, err)
22+
assert.True(t, found)
23+
assert.True(t, val)
24+
assert.NotContains(t, opts, "foo")
25+
26+
val, found, err = parseOptions[bool]("bar", opts)
27+
assert.NoError(t, err)
28+
assert.True(t, found)
29+
assert.False(t, val)
30+
assert.NotContains(t, opts, "bar")
31+
32+
val, found, err = parseOptions[bool]("baz", opts)
33+
assert.NoError(t, err)
34+
assert.True(t, found)
35+
assert.True(t, val)
36+
assert.NotContains(t, opts, "baz")
37+
38+
val, found, err = parseOptions[bool]("fizz", opts)
39+
assert.NoError(t, err)
40+
assert.True(t, found)
41+
assert.False(t, val)
42+
assert.NotContains(t, opts, "nofizz")
43+
44+
// Test []int64 option
45+
opts = map[string]string{
46+
"devices1": "0;1;2",
47+
"devices2": "3:4:5",
48+
"devices3": "6 7 8",
49+
"devices4": "9",
50+
"devices5": "",
51+
}
52+
53+
valList, found, err := parseOptions[[]int64]("devices1", opts)
54+
assert.NoError(t, err)
55+
assert.True(t, found)
56+
assert.Equal(t, []int64{0, 1, 2}, valList)
57+
58+
valList, found, err = parseOptions[[]int64]("devices2", opts)
59+
assert.NoError(t, err)
60+
assert.True(t, found)
61+
assert.Equal(t, []int64{3, 4, 5}, valList)
62+
63+
valList, found, err = parseOptions[[]int64]("devices3", opts)
64+
assert.NoError(t, err)
65+
assert.True(t, found)
66+
assert.Equal(t, []int64{6, 7, 8}, valList)
67+
68+
valList, found, err = parseOptions[[]int64]("devices4", opts)
69+
assert.NoError(t, err)
70+
assert.True(t, found)
71+
assert.Equal(t, []int64{9}, valList)
72+
73+
valList, found, err = parseOptions[[]int64]("devices5", opts)
74+
assert.NoError(t, err)
75+
assert.True(t, found)
76+
assert.Nil(t, valList)
77+
78+
// Test error cases
79+
opts = map[string]string{
80+
"bad_bool": "invalid",
81+
"bad_int": "1;foo",
82+
}
83+
84+
_, _, err = parseOptions[bool]("bad_bool", opts)
85+
assert.Error(t, err)
86+
87+
_, _, err = parseOptions[[]int64]("bad_int", opts)
88+
assert.Error(t, err)
89+
}

compute/xla/xla.go

Lines changed: 116 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
// - "allocator" (string, default="default"): which allocator to use. For CUDA the available ones are "default"
5656
// (== "bfc"), "bfc" ("best-fit for coalescing", avoids framementation), "cuda_async" (dynamic, no preallocation),
5757
// "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.
5860
//
5961
// # (NO) Dynamic Shapes
6062
//
@@ -113,6 +115,20 @@ func New(config string) (compute.Backend, error) {
113115
return NewWithOptions(config, nil)
114116
}
115117

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+
116132
// NewWithOptions creates a StableHLO backend with the given client options.
117133
// It allows more control, not available with the default New constructor.
118134
//
@@ -144,6 +160,13 @@ func NewWithOptions(config string, options pjrt.NamedValuesMap) (*Backend, error
144160
}
145161
}
146162

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+
147170
// FInd plugin.
148171
if !filepath.IsAbs(pluginName) {
149172
if autoInstall {
@@ -186,55 +209,57 @@ func NewWithOptions(config string, options pjrt.NamedValuesMap) (*Backend, error
186209
}
187210

188211
// Support "shared buffers":
189-
const shareBuffersName = "shared_buffers"
190-
if val := backendOptions[sharedBuffersName]; val != "" {
191-
b, err := strconv.ParseBool(val)
192-
if err != nil {
193-
return nil, errors.Wrapf(err, "Failed to parse option %q=%q", sharedBuffersName, val)
194-
}
212+
if b, found, err := parseOptions[bool]("shared_buffers", backendOptions); err != nil {
213+
return nil, err
214+
} else if found {
195215
backend.hasSharedBuffers = b
196-
delete(backendOptions, shareBuffersName)
197-
} else if _, ok := backendOptions["noshared_buffers"]; ok {
198-
backend.hasSharedBuffers = false
199-
delete(backendOptions, "noshared_buffers")
200216
}
201217

202218
// Support for tf32 DotGeneral.
203-
const tf32Name = "tf32"
204-
if val := backendOptions[tf32Name]; val != "" {
205-
backend.DotGeneralUseTF32 = true
206-
delete(backendOptions, "tf32")
207-
} else if _, ok := backendOptions["notf32"]; ok {
208-
// Deprecated, but still supported.
209-
backend.DotGeneralUseTF32 = false
210-
delete(backendOptions, "notf32")
219+
if b, found, err := parseOptions[bool]("tf32", backendOptions); err != nil {
220+
return nil, err
221+
} else if found {
222+
backend.DotGeneralUseTF32 = b
211223
}
212224

213225
// Support "preallocate":
214-
const preallocateName = "preallocate"
215-
if val := backendOptions[preallocateName]; val != "" {
216-
b, err := strconv.ParseBool(val)
217-
if err != nil {
218-
return nil, errors.Wrapf(err, "Failed to parse option %q=%q", preallocateName, val)
219-
}
220-
pluginOptions[preallocateName] = b
221-
delete(backendOptions, preallocateName)
226+
if b, found, err := parseOptions[bool]("preallocate", backendOptions); err != nil {
227+
return nil, err
228+
} else if found {
229+
pluginOptions["preallocate"] = b
222230
}
223231

224232
// Control memory fraction of preallocated memory:
225-
const memoryFractionName = "memory_fraction"
226-
if val := backendOptions[memoryFractionName]; val != "" {
227-
f, err := strconv.ParseFloat(val, 32)
228-
if err != nil {
229-
return nil, errors.Wrapf(err, "Failed to parse option %q=%q", memoryFractionName, val)
230-
}
231-
pluginOptions[memoryFractionName] = float32(f)
232-
delete(backendOptions, memoryFractionName)
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
233258
}
234259

235260
// Any leftover plugin options are unknown.
236261
if len(backendOptions) != 0 {
237-
klog.Errorf("backend %q: unknown plugin options %v", BackendName, backendOptions)
262+
klog.Errorf("backend %q: unknown plugin options %v", BackendName, xslices.SortedKeys(backendOptions))
238263
}
239264

240265
// Create plugin.
@@ -257,19 +282,66 @@ func NewWithOptions(config string, options pjrt.NamedValuesMap) (*Backend, error
257282
return backend, nil
258283
}
259284

260-
// parseOption parses the optionName from backendOptions (string).
285+
// parseOptions parses the optionName from backendOptions (string).
261286
// If optionName is found, it's removed from backendOptions.
262-
// It returns the parsed value and whether it was found.
263-
func parseOption[T interface{ string | bool | float32 }](
264-
optionName string, backendOptions map[string]string) (T, bool) {
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) {
265293
var val T
266-
valStr := backendOptions[optionName]
267-
if valStr == "" {
268-
return val, false
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
269306
}
270307
delete(backendOptions, optionName)
271-
switch any(val).type {
272-
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")
273345
}
274346
}
275347

compute/xla/xla_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,42 @@ func TestNewWithOptions(t *testing.T) {
113113
defer backend.Finalize()
114114
assert.True(t, backend.HasSharedBuffers())
115115
}
116+
117+
// Test cpu with noshared_buffers
118+
backend, err = xla.NewWithOptions("cpu,noshared_buffers", nil)
119+
if err == nil {
120+
defer backend.Finalize()
121+
assert.False(t, backend.HasSharedBuffers())
122+
}
123+
124+
// Test cpu with notf32
125+
backend, err = xla.NewWithOptions("cpu,notf32", nil)
126+
if err == nil {
127+
defer backend.Finalize()
128+
assert.False(t, backend.DotGeneralUseTF32)
129+
}
130+
131+
// Test cpu with tf32=false
132+
backend, err = xla.NewWithOptions("cpu,tf32=false", nil)
133+
if err == nil {
134+
defer backend.Finalize()
135+
assert.False(t, backend.DotGeneralUseTF32)
136+
}
137+
138+
// Test cpu with tf32 (no value, should default to true)
139+
backend, err = xla.NewWithOptions("cpu,tf32", nil)
140+
if err == nil {
141+
defer backend.Finalize()
142+
assert.True(t, backend.DotGeneralUseTF32)
143+
}
144+
145+
// Test help requested via pluginName
146+
_, err = xla.NewWithOptions("help", nil)
147+
assert.Error(t, err)
148+
assert.Equal(t, "Help requested", err.Error())
149+
150+
// Test help requested via option
151+
_, err = xla.NewWithOptions("cpu,help", nil)
152+
assert.Error(t, err)
153+
assert.Equal(t, "Help requested", err.Error())
116154
}

0 commit comments

Comments
 (0)