Skip to content

Commit a1528de

Browse files
jerryxyjOrbax Authors
authored andcommitted
No public description
PiperOrigin-RevId: 930675025
1 parent 3887ca6 commit a1528de

2 files changed

Lines changed: 161 additions & 5 deletions

File tree

export/orbax/export/data_processors/jax_data_processor.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,55 @@
2727
from .third_party.neptune.protos import manifest_pb2
2828

2929

30-
def _jax_spec_from(spec: Any) -> jax.ShapeDtypeStruct | Any:
30+
def _jax_spec_from(spec: Any) -> jax.ShapeDtypeStruct:
3131
"""Converts a ShloTensorSpec to a jax.ShapeDtypeStruct."""
3232
if isinstance(spec, shlo_function.ShloTensorSpec):
3333
if spec.dtype == shlo_function.ShloDType.bf16:
3434
return jax.ShapeDtypeStruct(spec.shape, jax.numpy.bfloat16)
3535
return jax.ShapeDtypeStruct(
3636
spec.shape, shlo_function.shlo_dtype_to_np_dtype(spec.dtype)
3737
)
38-
return spec
38+
if hasattr(spec, 'shape') and hasattr(spec, 'dtype'):
39+
return jax.ShapeDtypeStruct(
40+
shape=tuple(spec.shape),
41+
dtype=spec.dtype,
42+
)
43+
raise ValueError(f'Unsupported spec type: {type(spec)}')
44+
45+
46+
class _JaxShapeSpecGenerator:
47+
"""Generates unique shape spec strings for symbolic_args_specs."""
48+
49+
def __init__(self):
50+
self._counter = 0
51+
52+
def __call__(self, spec: Any) -> str:
53+
# PyTree leaves like int/float don't have a shape attribute.
54+
if hasattr(spec, 'shape'):
55+
if spec.shape is None:
56+
return '...'
57+
try:
58+
shape_list = list(spec.shape)
59+
except (TypeError, ValueError):
60+
return '...'
61+
if not shape_list:
62+
return '()'
63+
dims = []
64+
for i, d in enumerate(shape_list):
65+
if isinstance(d, str):
66+
dims.append(d)
67+
elif d is None:
68+
if i == 0:
69+
dims.append('b')
70+
else:
71+
dims.append(f'd_{self._counter}')
72+
self._counter += 1
73+
else:
74+
dims.append(str(d))
75+
if len(dims) == 1:
76+
return f'({dims[0]},)'
77+
return f"({', '.join(dims)})"
78+
raise ValueError(f'Unsupported spec type: {type(spec)}')
3979

4080

4181
class JaxDataProcessor(data_processor_base.DataProcessor):
@@ -112,7 +152,10 @@ def prepare(
112152

113153
self._input_signature = input_signature
114154

115-
jax_input_signature = jax.tree.map(_jax_spec_from, self._input_signature)
155+
jax_input_args = jax.tree.map(_jax_spec_from, self._input_signature)
156+
jax_input_shapes_specs = jax.tree.map(
157+
_JaxShapeSpecGenerator(), self._input_signature
158+
)
116159

117160
# Construct args_spec for jax2obm.convert.
118161
# We assume the callable takes (params, inputs) if params is not None,
@@ -123,7 +166,8 @@ def prepare(
123166
params_spec = jax.tree.map(
124167
lambda x: jax.ShapeDtypeStruct(x.shape, x.dtype), self._params
125168
)
126-
args_spec = (params_spec, jax_input_signature)
169+
args = (params_spec, jax_input_args)
170+
shapes_specs = ('...', jax_input_shapes_specs)
127171

128172
ckp_path = self._options.checkpoint_path or 'processor_checkpoint'
129173

@@ -143,7 +187,10 @@ def _save_checkpoint(
143187

144188
self._save_fn = _save_checkpoint
145189
else:
146-
args_spec = (jax_input_signature,)
190+
args = (jax_input_args,)
191+
shapes_specs = (jax_input_shapes_specs,)
192+
193+
args_spec = jax.export.symbolic_args_specs(args, shapes_specs)
147194

148195
# Instructs the runtime to only load the model parameters from the
149196
# checkpoint, not all keys present in the checkpoint.

export/orbax/export/data_processors/jax_data_processor_test.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from collections.abc import Mapping
1616
import pathlib
17+
import re
1718
from typing import Any
1819

1920
import jax
@@ -166,6 +167,114 @@ def add(x: jax.Array) -> jax.Array:
166167
('cpu', 'tpu'),
167168
)
168169

170+
def test_prepare_with_polymorphic_shapes(self):
171+
def add(x: jax.Array) -> jax.Array:
172+
return x + 1.0
173+
174+
processor = jax_data_processor.JaxDataProcessor(add, name='add')
175+
processor.prepare(
176+
jax.ShapeDtypeStruct(('b', 3), jnp.float32),
177+
)
178+
179+
self.assertIsNotNone(processor.obm_function)
180+
self.assertIsNotNone(processor.input_signature)
181+
self.assertIsNotNone(processor.output_signature)
182+
183+
def test_prepare_with_polymorphic_shapes_signatures(self):
184+
def add(x: jax.Array) -> jax.Array:
185+
return x + 1.0
186+
187+
processor = jax_data_processor.JaxDataProcessor(add, name='add')
188+
processor.prepare(
189+
jax.ShapeDtypeStruct(('b', 3), jnp.float32),
190+
)
191+
192+
self.assertEqual(
193+
processor.input_signature, jax.ShapeDtypeStruct(('b', 3), jnp.float32)
194+
)
195+
196+
# Note: The underlying OBM Function signature uses None for dynamic
197+
# dimensions, regardless of the symbolic string provided by the user.
198+
out_spec = processor.output_signature
199+
self.assertEqual(list(out_spec.shape), [None, 3]) # pytype: disable=attribute-error
200+
201+
def test_prepare_with_polymorphic_shapes_none(self):
202+
def add(x: jax.Array) -> jax.Array:
203+
return x + 1.0
204+
205+
processor = jax_data_processor.JaxDataProcessor(add, name='add')
206+
processor.prepare(
207+
jax.ShapeDtypeStruct((None, 3), jnp.float32),
208+
)
209+
210+
self.assertEqual(
211+
processor.input_signature, jax.ShapeDtypeStruct((None, 3), jnp.float32)
212+
)
213+
214+
out_spec = processor.output_signature
215+
self.assertEqual(list(out_spec.shape), [None, 3]) # pytype: disable=attribute-error
216+
217+
218+
class JaxShapeSpecGeneratorTest(parameterized.TestCase):
219+
220+
@parameterized.named_parameters(
221+
dict(testcase_name='empty_shape', shape=(), expected='()'),
222+
dict(testcase_name='one_dim_none', shape=(None,), expected='(b,)'),
223+
dict(testcase_name='one_dim_int', shape=(4,), expected='(4,)'),
224+
dict(
225+
testcase_name='multi_dim_first_none',
226+
shape=(None, 4),
227+
expected='(b, 4)',
228+
),
229+
dict(
230+
testcase_name='multi_dim_second_none',
231+
shape=(4, None),
232+
expected='(4, d_0)',
233+
),
234+
dict(
235+
testcase_name='multi_dim_both_none',
236+
shape=(None, None),
237+
expected='(b, d_0)',
238+
),
239+
dict(
240+
testcase_name='multi_dim_all_none',
241+
shape=(None, None, None, 256),
242+
expected='(b, d_0, d_1, 256)',
243+
),
244+
dict(testcase_name='string_dims', shape=('foo', 4), expected='(foo, 4)'),
245+
dict(
246+
testcase_name='string_and_none',
247+
shape=('foo', None),
248+
expected='(foo, d_0)',
249+
),
250+
)
251+
def test_jax_shape_spec_generator(self, expected, shape=None):
252+
spec = jax.ShapeDtypeStruct(shape, jnp.float32)
253+
generator = jax_data_processor._JaxShapeSpecGenerator()
254+
self.assertEqual(generator(spec), expected)
255+
256+
def test_jax_shape_spec_generator_unsupported_type(self):
257+
spec = object()
258+
generator = jax_data_processor._JaxShapeSpecGenerator()
259+
with self.assertRaisesRegex(
260+
ValueError, f'Unsupported spec type: {re.escape(str(type(spec)))}'
261+
):
262+
generator(spec)
263+
264+
def test_jax_shape_spec_generator_multiple_calls(self):
265+
spec1 = jax.ShapeDtypeStruct((None, None), jnp.float32)
266+
spec2 = jax.ShapeDtypeStruct((None, None, 256), jnp.float32)
267+
generator = jax_data_processor._JaxShapeSpecGenerator()
268+
self.assertEqual(generator(spec1), '(b, d_0)')
269+
self.assertEqual(generator(spec2), '(b, d_1, 256)')
270+
271+
def test_jax_shape_spec_generator_uniterable_shape(self):
272+
class UniterableShape:
273+
shape = None
274+
275+
generator = jax_data_processor._JaxShapeSpecGenerator()
276+
self.assertEqual(generator(UniterableShape()), '...')
277+
169278

170279
if __name__ == '__main__':
171280
googletest.main()

0 commit comments

Comments
 (0)