Skip to content

Commit 8530fd0

Browse files
eb8680jfesercooijmanstimkiranandcode
authored
Update staging-llm from master (#457)
* Release v0.2.3 (#374) * Install prettyprinter for term when library is available (#386) * install prettyprinter for term when library is available * lint * move code into types.py * fix pypandoc issue (#397) * Convert evaluate to a singledispatch (#398) * convert evaluate to a singledispatch * lint * add jnp.pi and ArrayTerm.T (#394) * Deprecate defterm (#399) * deprecate defterm * remove defterm case * remove defterm * lint * evaluate distribution arguments * lint * remove interpreter * Revert "remove interpreter" This reverts commit 3044277. * wip * lint * Rework numpyro distribution handling to enable symbolic distributions and handling of distribution methods (#311) * refactor distribution operations * add a test for typeof of distributions * add tests for symbolic dists/arguments * introduce operations for distribution methods * comment * fix tests * work around #310 * replace hack with new hack * tweak repr for _BaseOperation * lint * work around #312 * clean up access to dist ops * wip * wip * add type annotations to get correct term conversion * lint * include distribution arguments as properties * fix distribution calls * try again * fixes * format * Box the output of `__type_rule__` (#387) * box the output of __type_rule__ * fix tests * fix tests * require callers of __type_rule__ to box arguments * fix * move Box out of ops.types * lint * fix test * fix syntactic_eq implementation for jax arrays (#405) * Fix recursion error in sizesof (#406) * fix recursion error in sizesof * format * Allow `_BaseOperation` subclasses to have an overrideable `apply` method (#414) * stash * fixes * initial * wip * lint * ensure each subclass has a fresh operation * wip * wip * lint * wip * wip * lint * refactor class method support * move defops * fix test * remove singledispatch case and add test * move definition * cleanup * simplify * cleanup * lint * fix failing test * fix classmethod * __isabstractmethod__ * revert --------- Co-authored-by: Eli <eli@basis.ai> * Try pulling in pyproject.toml from staging-llm to master (#425) * Generate instance-level `Operation`s for bound methods (#351) * generalize __get__ * nits * coverage of methoddescriptor api * methodtype * simplify * simplify * simplify * format * revert * restore * simplify * simplify * retain instance op on term construction * Simplify apply inheritance * assign * put call next to init_subclass * add explanatory comment * Operation.apply -> Operation.__apply__ * add test based on issue description * fix doctest * Fix dataclass @defops and added dataclass metaclass (#439) * fixed dataclass ordering and added metaclass for simplifying construction of dataclass terms * ensure term fields are not being overriden * added decorator and dataclass * updated to make defdata registration automatic * simplified dataclass loop * updated to give property op an appropriate name * added failing tests * fixed failing test * fixed numpyro/pyro/torch interfaces * minor fix + test for deffn kwargs * Type check and lint example code (#449) * format example code * type check examples * Add beam search example using thermometer continuations (#431) * add beam search example using thermometer continuations * address comments * add docstring * lint * Fix for jax 0.8.2 (#455) * fix for jax 0.8.2 * add more register * format --------- Co-authored-by: Jack Feser <jack.feser@gmail.com> Co-authored-by: Tim Cooijmans <cooijmans.tim@gmail.com> Co-authored-by: Kiran Gopinathan <23038502+kiranandcode@users.noreply.github.com>
1 parent 931d507 commit 8530fd0

11 files changed

Lines changed: 206 additions & 28 deletions

File tree

docs/source/beam.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""This example demonstrates a beam search over a program that uses a `choose`
2+
effect for nondeterminism and `score` effect to weigh its choices.
3+
4+
"""
5+
6+
import functools
7+
import heapq
8+
import random
9+
from collections.abc import Callable
10+
from dataclasses import dataclass
11+
from pprint import pprint
12+
13+
from effectful.ops.semantics import fwd, handler
14+
from effectful.ops.syntax import ObjectInterpretation, defop, implements
15+
16+
17+
@defop
18+
def choose[T](choices: list[T]) -> T:
19+
result = random.choice(choices)
20+
print(f"choose({choices}) = {result}")
21+
return result
22+
23+
24+
@defop
25+
def score(value: float) -> None:
26+
pass
27+
28+
29+
class Suspend(Exception): ...
30+
31+
32+
class ReplayIntp(ObjectInterpretation):
33+
def __init__(self, trace):
34+
self.trace = trace
35+
self.step = 0
36+
37+
@implements(choose)
38+
def _(self, *args, **kwargs):
39+
if self.step < len(self.trace):
40+
result = self.trace[self.step][1]
41+
self.step += 1
42+
return result
43+
return fwd()
44+
45+
46+
class TraceIntp(ObjectInterpretation):
47+
def __init__(self):
48+
self.trace = []
49+
50+
@implements(choose)
51+
def _(self, *args, **kwargs):
52+
result = fwd()
53+
self.trace.append(((args, kwargs), result))
54+
return result
55+
56+
57+
class ScoreIntp(ObjectInterpretation):
58+
def __init__(self):
59+
self.score = 0.0
60+
61+
@implements(score)
62+
def _(self, value):
63+
self.score += value
64+
65+
66+
class ChooseOnceIntp(ObjectInterpretation):
67+
def __init__(self):
68+
self.is_first_call = True
69+
70+
@implements(choose)
71+
def _(self, *args, **kwargs):
72+
if not self.is_first_call:
73+
raise Suspend
74+
75+
self.is_first_call = False
76+
return fwd()
77+
78+
79+
@dataclass
80+
class BeamCandidate[S, T]:
81+
"""Represents a candidate execution path in beam search."""
82+
83+
trace: list[S]
84+
score: float
85+
in_progress: bool
86+
result: T | None
87+
88+
def __lt__(self, other: "BeamCandidate[S, T]") -> bool:
89+
return self.score < other.score
90+
91+
def expand[**P](self, model_fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs):
92+
in_progress = False
93+
result = None
94+
score_intp = ScoreIntp()
95+
trace_intp = TraceIntp()
96+
with (
97+
handler(score_intp),
98+
handler(ChooseOnceIntp()),
99+
handler(ReplayIntp(self.trace)),
100+
handler(trace_intp),
101+
):
102+
try:
103+
result = model_fn(*args, **kwargs)
104+
except Suspend:
105+
in_progress = True
106+
107+
return BeamCandidate(trace_intp.trace, score_intp.score, in_progress, result)
108+
109+
110+
def beam_search[**P, S, T](
111+
model_fn: Callable[P, T], beam_width=3
112+
) -> Callable[P, BeamCandidate[S, T]]:
113+
@functools.wraps(model_fn)
114+
def wrapper(*args, **kwargs):
115+
beam = [BeamCandidate([], 0.0, True, None)]
116+
117+
while True:
118+
expandable = [c for c in beam if c.in_progress] * beam_width
119+
if not expandable:
120+
return beam
121+
122+
new_candidates = [c.expand(model_fn, *args, **kwargs) for c in expandable]
123+
124+
for c in new_candidates:
125+
heapq.heappushpop(beam, c) if len(
126+
beam
127+
) >= beam_width else heapq.heappush(beam, c)
128+
129+
return wrapper
130+
131+
132+
if __name__ == "__main__":
133+
134+
def model():
135+
s1 = choose(range(100))
136+
score(s1)
137+
s2 = choose(range(-100, 100))
138+
score(s2)
139+
s3 = choose(range(-100, 100))
140+
score(s3)
141+
return s3
142+
143+
result: BeamCandidate = beam_search(model)()
144+
pprint(result)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Angelic Nondeterminism
2+
======================
3+
4+
Here we give an example of *angelic nondeterminism* in effectful [#f1]_.
5+
Our model is a nondeterministic program that makes choices using a ``choose`` effect and uses a ``score`` effect to sum up a final score.
6+
We implement a beam search that optimizes this final score as a handler for the ``choose`` and ``score`` effects.
7+
8+
The beam search works by running the model until it reaches a ``choose``, at which point the continuation is captured.
9+
This continuation is resumed multiple times with different values from ``choose`` to expand the beam.
10+
The intermediate score is used to rank the beam candidates.
11+
12+
Because Python does not have support for first-class continuations, we use *thermometer continuations* [#f2]_.
13+
A thermometer continuation works by tracking any nondeterminism
14+
(essentially, the model is rerun from the start replaying the ``choose`` effects).
15+
If ``choose`` is the only source of nondeterminism, then the
16+
after each ``choose`` and replaying it uses *thermometer continuations* to
17+
18+
.. literalinclude:: ./beam.py
19+
:language: python
20+
21+
References
22+
----------
23+
24+
.. [#f1] Li, Z., Solar-Lezama, A., Yue, Y., and Zheng, S., "EnCompass: Enhancing Agent Programming with Search Over Program Execution Paths", 2025. https://arxiv.org/abs/2512.03571
25+
26+
.. [#f2] James Koppel, Gabriel Scherer, and Armando Solar-Lezama. 2018. Capturing the future by replaying the past (functional pearl). Proc. ACM Program. Lang. 2, ICFP, Article 76 (September 2018), 29 pages. https://doi.org/10.1145/3236771

docs/source/conf.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212

1313
import os
1414
import sys
15-
from typing import List
1615

1716
sys.path.insert(0, os.path.abspath("../../"))
18-
import sphinx_rtd_theme # noqa: E402
1917

2018
# -- Project information -----------------------------------------------------
2119

@@ -69,7 +67,7 @@
6967
# List of patterns, relative to source directory, that match files and
7068
# directories to ignore when looking for source files.
7169
# This pattern also affects html_static_path and html_extra_path.
72-
exclude_patterns: List[str] = []
70+
exclude_patterns: list[str] = []
7371

7472

7573
# -- Options for HTML output -------------------------------------------------

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Table of Contents
1616
minipyro_example
1717
lambda_example
1818
semi_ring_example
19+
beam_search_example
1920

2021
.. toctree::
2122
:maxdepth: 2

docs/source/lambda_.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
2-
from typing import Annotated, Callable
2+
from collections.abc import Callable
3+
from typing import Annotated
34

45
from effectful.ops.semantics import coproduct, evaluate, fvsof, fwd, handler
56
from effectful.ops.syntax import Scoped, defdata, defop, syntactic_eq

docs/source/minipyro.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
"""
1515

1616
import random
17+
from collections import OrderedDict
18+
from collections.abc import Callable
1719
from contextlib import contextmanager
1820
from dataclasses import dataclass
1921
from functools import partial
20-
from typing import Callable, Concatenate, NamedTuple, Optional, OrderedDict, Union
22+
from typing import Concatenate, NamedTuple
2123
from weakref import ref
2224

2325
import numpy as np
@@ -40,7 +42,6 @@
4042
from effectful.ops.syntax import ObjectInterpretation, defop, implements
4143
from effectful.ops.types import Operation
4244

43-
4445
# Poutine has a notion of 'messages', which are dictionaries
4546
# that are passed between handlers (or 'Messengers') in order
4647
# to facilitate coordination and composition using "magic" slots.
@@ -56,7 +57,7 @@ class SampleMsg:
5657
name: str
5758
val: Tensor
5859
dist: Distribution
59-
obs: Optional[Tensor]
60+
obs: Tensor | None
6061

6162

6263
@dataclass
@@ -65,7 +66,7 @@ class ParamMsg:
6566
val: Tensor
6667

6768

68-
Message = Union[ParamMsg, SampleMsg]
69+
Message = ParamMsg | SampleMsg
6970
Trace = OrderedDict[str, Message]
7071

7172

@@ -87,16 +88,16 @@ class Seed(NamedTuple):
8788

8889

8990
@defop
90-
def sample(name: str, dist: Distribution, obs: Optional[Tensor] = None) -> Tensor:
91+
def sample(name: str, dist: Distribution, obs: Tensor | None = None) -> Tensor:
9192
raise RuntimeError("No default implementation of sample")
9293

9394

9495
@defop
9596
def param(
9697
var_name: str,
97-
initial_value: Optional[Union[Tensor, Callable[[], Tensor]]] = None,
98-
constraint: Optional[Constraint] = None,
99-
event_dim: Optional[int] = None,
98+
initial_value: Tensor | Callable[[], Tensor] | None = None,
99+
constraint: Constraint | None = None,
100+
event_dim: int | None = None,
100101
) -> Tensor:
101102
raise RuntimeError("No default implementation of param")
102103

@@ -123,7 +124,7 @@ def get_rng_seed() -> Seed:
123124

124125

125126
@defop
126-
def set_rng_seed(seed: Union[int, Seed]):
127+
def set_rng_seed(seed: int | Seed):
127128
raise RuntimeError("No default implementation of get_rng_seed")
128129

129130

@@ -165,9 +166,9 @@ def sample(self, var_name: str, dist: Distribution, **kwargs):
165166
def param(
166167
self,
167168
var_name: str,
168-
initial_value: Optional[Union[Tensor, Callable[[], Tensor]]] = None,
169-
constraint: Optional[Constraint] = None,
170-
event_dim: Optional[int] = None,
169+
initial_value: Tensor | Callable[[], Tensor] | None = None,
170+
constraint: Constraint | None = None,
171+
event_dim: int | None = None,
171172
) -> Tensor:
172173
# Similar to `Tracer.sample`
173174

@@ -227,7 +228,7 @@ def get_rng_seed(self):
227228
)
228229

229230
@implements(set_rng_seed)
230-
def set_rng_seed(self, seed: Union[int, Seed]):
231+
def set_rng_seed(self, seed: int | Seed):
231232
if isinstance(seed, int):
232233
manual_seed(seed)
233234
random.seed(seed)
@@ -276,9 +277,9 @@ def __init__(self, initial_store=None):
276277
def param(
277278
self,
278279
name: str,
279-
initial_value: Union[Tensor, None, Callable[[], Tensor]] = None,
280+
initial_value: Tensor | None | Callable[[], Tensor] = None,
280281
constraint: Constraint = distributions.constraints.real,
281-
event_dim: Optional[int] = None,
282+
event_dim: int | None = None,
282283
) -> Tensor:
283284
if event_dim is not None:
284285
raise RuntimeError("minipyro.plate does not support the event_dim arg")
@@ -321,7 +322,7 @@ class Plate(ObjectInterpretation):
321322
An `Interpretation` which automatically broadcasts the `sample` `Operation`
322323
"""
323324

324-
def __init__(self, name: str, size: int, dim: Optional[int]):
325+
def __init__(self, name: str, size: int, dim: int | None):
325326
if dim is None:
326327
raise ValueError("mini-pyro requires the `dim` argument to `plate`")
327328

@@ -342,7 +343,7 @@ def do_sample(self, sampled_name: str, dist: Distribution, **kwargs) -> Tensor:
342343

343344

344345
# Helper for using `Plate` as a `handler`
345-
def plate(name: str, size: int, dim: Optional[int] = None):
346+
def plate(name: str, size: int, dim: int | None = None):
346347
return handler(Plate(name, size, dim))
347348

348349

@@ -354,7 +355,7 @@ def plate(name: str, size: int, dim: Optional[int] = None):
354355

355356

356357
def block[**P](
357-
hide_fn: Callable[Concatenate[Operation, object, P], bool] = lambda *_, **__: True
358+
hide_fn: Callable[Concatenate[Operation, object, P], bool] = lambda *_, **__: True,
358359
):
359360
"""
360361
Block is a helper for masking out a subset of calls to either
@@ -372,7 +373,7 @@ def blocking(op: Operation, *args, **kwargs):
372373
return op(*args, **kwargs)
373374
return fwd()
374375

375-
return handler({sample: partial(blocking, sample), param: partial(blocking, param)}) # type: ignore
376+
return handler({sample: partial(blocking, sample), param: partial(blocking, param)})
376377

377378

378379
# This is a thin wrapper around the `torch.optim.Adam` class that

docs/source/readme_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def assoc_add(x, y):
3535
commute_rules = {add: commute_add}
3636
assoc_rules = {add: assoc_add}
3737

38-
eager_mixed = functools.reduce(coproduct, (beta_rules, commute_rules, assoc_rules))
38+
eager_mixed = functools.reduce(coproduct, (beta_rules, commute_rules, assoc_rules)) # type: ignore
3939

4040
x = defop(int, name="x")
4141
y = defop(int, name="y")

docs/source/semi_ring.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import collections.abc
22
import operator
33
import types
4-
from typing import Annotated, Tuple, Union, cast, overload
4+
from typing import Annotated, cast, overload
55

66
from effectful.ops.semantics import coproduct, evaluate, fwd, handler
77
from effectful.ops.syntax import Scoped, defop

effectful/handlers/jax/_terms.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,8 @@ def ndim(self) -> int:
432432
return len(self.shape)
433433

434434

435-
@bind_dims.register # type: ignore
435+
@bind_dims.register(jax.Array) # type: ignore
436+
@bind_dims.register(jax._src.core.Tracer) # type: ignore
436437
def _bind_dims_array(t: jax.Array, *args: Operation[[], jax.Array]) -> jax.Array:
437438
"""Convert named dimensions to positional dimensions.
438439
@@ -501,6 +502,7 @@ def _evaluate(expr):
501502
return reindexed
502503

503504

504-
@unbind_dims.register # type: ignore
505+
@unbind_dims.register(jax.Array) # type: ignore
506+
@unbind_dims.register(jax._src.core.Tracer) # type: ignore
505507
def _unbind_dims_array(t: jax.Array, *args: Operation[[], jax.Array]) -> jax.Array:
506508
return jax_getitem(t, tuple(n() for n in args))

scripts/clean.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
set -euxo pipefail
33

4-
SRC="effectful tests"
4+
SRC="effectful tests docs/source"
55
ruff check --fix $SRC
66
ruff format $SRC
77

0 commit comments

Comments
 (0)