Skip to content

Commit 9dca632

Browse files
committed
format
1 parent 251a742 commit 9dca632

2 files changed

Lines changed: 85 additions & 32 deletions

File tree

tesseract_core/runtime/experimental.py

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
import json
55
from collections.abc import Callable, Iterator, Sequence
66
from pathlib import Path
7-
8-
import numpy as np
97
from typing import (
108
Annotated,
119
Any,
1210
get_args,
1311
get_origin,
1412
)
1513

14+
import numpy as np
1615
from pydantic import (
1716
AfterValidator,
1817
GetCoreSchemaHandler,
@@ -29,7 +28,6 @@
2928
log_parameter,
3029
)
3130
from tesseract_core.runtime.schema_types import safe_issubclass
32-
from tesseract_core.runtime.tree_transforms import get_at_path
3331

3432
# Finite difference utilities for automatic differentiation
3533
# These provide a simple way to make any Tesseract differentiable without
@@ -40,13 +38,19 @@
4038
finite_difference_jvp,
4139
finite_difference_vjp,
4240
)
43-
41+
from tesseract_core.runtime.tree_transforms import get_at_path
4442

4543
# Autodiff fallback utilities for deriving missing autodiff endpoints from existing ones.
4644
# These are experimental and the API may change in future releases.
4745

4846

49-
def vjp_from_jacobian(jacobian_fn, inputs, vjp_inputs, vjp_outputs, cotangent_vector):
47+
def vjp_from_jacobian(
48+
jacobian_fn: Callable,
49+
inputs: Any,
50+
vjp_inputs: set[str],
51+
vjp_outputs: set[str],
52+
cotangent_vector: dict[str, Any],
53+
) -> dict[str, Any]:
5054
"""Compute VJP as v^T @ J using the full Jacobian.
5155
5256
Args:
@@ -64,15 +68,21 @@ def vjp_from_jacobian(jacobian_fn, inputs, vjp_inputs, vjp_outputs, cotangent_ve
6468
for dx in vjp_inputs:
6569
grad = None
6670
for dy in vjp_outputs:
67-
J = np.asarray(jac[dy][dx]) # shape: (*dy_shape, *dx_shape)
68-
v = np.asarray(cotangent_vector[dy]) # shape: (*dy_shape)
71+
J = np.asarray(jac[dy][dx]) # shape: (*dy_shape, *dx_shape)
72+
v = np.asarray(cotangent_vector[dy]) # shape: (*dy_shape)
6973
term = np.tensordot(v, J, axes=v.ndim)
7074
grad = term if grad is None else grad + term
7175
out[dx] = grad
7276
return out
7377

7478

75-
def jvp_from_jacobian(jacobian_fn, inputs, jvp_inputs, jvp_outputs, tangent_vector):
79+
def jvp_from_jacobian(
80+
jacobian_fn: Callable,
81+
inputs: Any,
82+
jvp_inputs: set[str],
83+
jvp_outputs: set[str],
84+
tangent_vector: dict[str, Any],
85+
) -> dict[str, Any]:
7686
"""Compute JVP as J @ t using the full Jacobian.
7787
7888
Args:
@@ -90,15 +100,21 @@ def jvp_from_jacobian(jacobian_fn, inputs, jvp_inputs, jvp_outputs, tangent_vect
90100
for dy in jvp_outputs:
91101
result = None
92102
for dx in jvp_inputs:
93-
J = np.asarray(jac[dy][dx]) # shape: (*dy_shape, *dx_shape)
94-
t = np.asarray(tangent_vector[dx]) # shape: (*dx_shape)
103+
J = np.asarray(jac[dy][dx]) # shape: (*dy_shape, *dx_shape)
104+
t = np.asarray(tangent_vector[dx]) # shape: (*dx_shape)
95105
term = np.tensordot(J, t, axes=t.ndim)
96106
result = term if result is None else result + term
97107
out[dy] = result
98108
return out
99109

100110

101-
def jacobian_from_vjp(vjp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
111+
def jacobian_from_vjp(
112+
vjp_fn: Callable,
113+
apply_fn: Callable,
114+
inputs: Any,
115+
jac_inputs: set[str],
116+
jac_outputs: set[str],
117+
) -> dict[str, dict[str, Any]]:
102118
"""Compute the Jacobian by calling VJP with one-hot cotangent vectors.
103119
104120
Requires M calls to VJP, where M is the total number of output elements.
@@ -115,7 +131,9 @@ def jacobian_from_vjp(vjp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
115131
where each array has shape (*output_shape, *input_shape).
116132
"""
117133
raw_outputs = apply_fn(inputs=inputs)
118-
outputs_dict = raw_outputs.model_dump() if hasattr(raw_outputs, "model_dump") else raw_outputs
134+
outputs_dict = (
135+
raw_outputs.model_dump() if hasattr(raw_outputs, "model_dump") else raw_outputs
136+
)
119137

120138
jac = {}
121139
output_vals = {}
@@ -130,7 +148,7 @@ def jacobian_from_vjp(vjp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
130148
for dy in jac_outputs:
131149
dy_val = output_vals[dy]
132150
dy_shape = dy_val.shape
133-
for nd_idx in (np.ndindex(*dy_shape) if dy_shape else [()]):
151+
for nd_idx in np.ndindex(*dy_shape) if dy_shape else [()]:
134152
cotangent = {dy: np.zeros_like(dy_val)}
135153
if dy_shape:
136154
cotangent[dy][nd_idx] = 1.0
@@ -150,7 +168,13 @@ def jacobian_from_vjp(vjp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
150168
return jac
151169

152170

153-
def jacobian_from_jvp(jvp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
171+
def jacobian_from_jvp(
172+
jvp_fn: Callable,
173+
apply_fn: Callable,
174+
inputs: Any,
175+
jac_inputs: set[str],
176+
jac_outputs: set[str],
177+
) -> dict[str, dict[str, Any]]:
154178
"""Compute the Jacobian by calling JVP with one-hot tangent vectors.
155179
156180
Requires N calls to JVP, where N is the total number of input elements.
@@ -167,7 +191,9 @@ def jacobian_from_jvp(jvp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
167191
where each array has shape (*output_shape, *input_shape).
168192
"""
169193
raw_outputs = apply_fn(inputs=inputs)
170-
outputs_dict = raw_outputs.model_dump() if hasattr(raw_outputs, "model_dump") else raw_outputs
194+
outputs_dict = (
195+
raw_outputs.model_dump() if hasattr(raw_outputs, "model_dump") else raw_outputs
196+
)
171197

172198
jac = {}
173199
for dy in jac_outputs:
@@ -180,7 +206,7 @@ def jacobian_from_jvp(jvp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
180206
for dx in jac_inputs:
181207
dx_val = np.asarray(get_at_path(inputs, dx))
182208
dx_shape = dx_val.shape
183-
for nd_idx in (np.ndindex(*dx_shape) if dx_shape else [()]):
209+
for nd_idx in np.ndindex(*dx_shape) if dx_shape else [()]:
184210
tangent = {dx: np.zeros_like(dx_val)}
185211
if dx_shape:
186212
tangent[dx][nd_idx] = 1.0
@@ -195,7 +221,7 @@ def jacobian_from_jvp(jvp_fn, apply_fn, inputs, jac_inputs, jac_outputs):
195221
for dy in jac_outputs:
196222
dy_result = np.asarray(result[dy])
197223
if dx_shape:
198-
jac[dy][dx][(...,) + nd_idx] = dy_result
224+
jac[dy][dx][(..., *nd_idx)] = dy_result
199225
else:
200226
jac[dy][dx] = dy_result
201227
return jac
@@ -514,10 +540,9 @@ def __get_pydantic_json_schema__(
514540
"jacobian_from_jvp",
515541
"jacobian_from_vjp",
516542
"jvp_from_jacobian",
517-
"vjp_from_jacobian",
518543
"log_artifact",
519544
"log_metric",
520545
"log_parameter",
521546
"require_file",
547+
"vjp_from_jacobian",
522548
]
523-

tests/runtime_tests/test_autodiff_fallbacks.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
vjp_from_jacobian,
2424
)
2525

26-
# A fixed 2×3 linear map: f(x) = A @ x, Jacobian is A everywhere.
26+
# A fixed 2x3 linear map: f(x) = A @ x, Jacobian is A everywhere.
2727
_A = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32)
2828

2929
_linear_input = {"x": np.array([1.0, 0.0, 0.0], dtype=np.float32)}
@@ -70,18 +70,28 @@ def apply(self, inputs):
7070
def jacobian(self, inputs, jac_inputs, jac_outputs):
7171
return {"y": {"x": _A.copy()}}
7272

73-
def jacobian_vector_product(self, inputs, jvp_inputs, jvp_outputs, tangent_vector):
74-
return jvp_from_jacobian(self.jacobian, inputs, jvp_inputs, jvp_outputs, tangent_vector)
73+
def jacobian_vector_product(
74+
self, inputs, jvp_inputs, jvp_outputs, tangent_vector
75+
):
76+
return jvp_from_jacobian(
77+
self.jacobian, inputs, jvp_inputs, jvp_outputs, tangent_vector
78+
)
7579

76-
def vector_jacobian_product(self, inputs, vjp_inputs, vjp_outputs, cotangent_vector):
77-
return vjp_from_jacobian(self.jacobian, inputs, vjp_inputs, vjp_outputs, cotangent_vector)
80+
def vector_jacobian_product(
81+
self, inputs, vjp_inputs, vjp_outputs, cotangent_vector
82+
):
83+
return vjp_from_jacobian(
84+
self.jacobian, inputs, vjp_inputs, vjp_outputs, cotangent_vector
85+
)
7886

7987
return LinearModule("LinearModule")
8088

8189

8290
def test_jvp_via_experimental_helper(module_jac_with_derived_jvp_vjp):
8391
endpoints = create_endpoints(module_jac_with_derived_jvp_vjp)
84-
endpoint_func, EndpointSchema, _ = _find_endpoint(endpoints, "jacobian_vector_product")
92+
endpoint_func, EndpointSchema, _ = _find_endpoint(
93+
endpoints, "jacobian_vector_product"
94+
)
8595

8696
payload = EndpointSchema.model_validate(
8797
{
@@ -98,7 +108,9 @@ def test_jvp_via_experimental_helper(module_jac_with_derived_jvp_vjp):
98108

99109
def test_vjp_via_experimental_helper(module_jac_with_derived_jvp_vjp):
100110
endpoints = create_endpoints(module_jac_with_derived_jvp_vjp)
101-
endpoint_func, EndpointSchema, _ = _find_endpoint(endpoints, "vector_jacobian_product")
111+
endpoint_func, EndpointSchema, _ = _find_endpoint(
112+
endpoints, "vector_jacobian_product"
113+
)
102114

103115
payload = EndpointSchema.model_validate(
104116
{
@@ -140,19 +152,27 @@ def OutputSchema(self):
140152
def apply(self, inputs):
141153
return _OutputSchema(y=_A @ np.asarray(inputs.x))
142154

143-
def jacobian_vector_product(self, inputs, jvp_inputs, jvp_outputs, tangent_vector):
155+
def jacobian_vector_product(
156+
self, inputs, jvp_inputs, jvp_outputs, tangent_vector
157+
):
144158
t = np.asarray(tangent_vector["x"])
145159
return {"y": _A @ t}
146160

147161
def jacobian(self, inputs, jac_inputs, jac_outputs):
148162
return jacobian_from_jvp(
149-
self.jacobian_vector_product, self.apply, inputs, jac_inputs, jac_outputs
163+
self.jacobian_vector_product,
164+
self.apply,
165+
inputs,
166+
jac_inputs,
167+
jac_outputs,
150168
)
151169

152170
return LinearModule("LinearModule")
153171

154172

155-
def test_jacobian_derived_from_jvp_via_experimental_helper(module_jvp_with_derived_jacobian):
173+
def test_jacobian_derived_from_jvp_via_experimental_helper(
174+
module_jvp_with_derived_jacobian,
175+
):
156176
endpoints = create_endpoints(module_jvp_with_derived_jacobian)
157177
endpoint_func, EndpointSchema, _ = _find_endpoint(endpoints, "jacobian")
158178

@@ -194,19 +214,27 @@ def OutputSchema(self):
194214
def apply(self, inputs):
195215
return _OutputSchema(y=_A @ np.asarray(inputs.x))
196216

197-
def vector_jacobian_product(self, inputs, vjp_inputs, vjp_outputs, cotangent_vector):
217+
def vector_jacobian_product(
218+
self, inputs, vjp_inputs, vjp_outputs, cotangent_vector
219+
):
198220
v = np.asarray(cotangent_vector["y"])
199221
return {"x": _A.T @ v}
200222

201223
def jacobian(self, inputs, jac_inputs, jac_outputs):
202224
return jacobian_from_vjp(
203-
self.vector_jacobian_product, self.apply, inputs, jac_inputs, jac_outputs
225+
self.vector_jacobian_product,
226+
self.apply,
227+
inputs,
228+
jac_inputs,
229+
jac_outputs,
204230
)
205231

206232
return LinearModule("LinearModule")
207233

208234

209-
def test_jacobian_derived_from_vjp_via_experimental_helper(module_vjp_with_derived_jacobian):
235+
def test_jacobian_derived_from_vjp_via_experimental_helper(
236+
module_vjp_with_derived_jacobian,
237+
):
210238
endpoints = create_endpoints(module_vjp_with_derived_jacobian)
211239
endpoint_func, EndpointSchema, _ = _find_endpoint(endpoints, "jacobian")
212240

0 commit comments

Comments
 (0)