Skip to content

Commit fe2f1ad

Browse files
committed
refactor: replace pysimdjson with orjson
pysimdjson does not build from source on Python 3.14, macOS arm64. orjson seems better maintained. orjson is about 20% slower than pysimdjson.
1 parent 9649d13 commit fe2f1ad

4 files changed

Lines changed: 36 additions & 45 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
python = "^3.12"
2626
aiohttp = "^3.6"
2727
httpstan = "~4.17"
28-
pysimdjson = ">=5.0.2"
28+
orjson = ">=3.10"
2929
numpy = ">=1.19"
3030
clikit = "^0.6"
3131
setuptools = "*"

stan/common.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import aiohttp
77
import aiohttp.web
88
import httpstan.app
9-
import simdjson
9+
import orjson
1010

1111

1212
def unused_tcp_port():
@@ -22,8 +22,7 @@ class HTTPResponse(typing.NamedTuple):
2222
content: bytes
2323

2424
def json(self) -> dict:
25-
# mypy 0.961 complains that simdjson lacks a `loads`.
26-
return simdjson.loads(self.content) # type: ignore
25+
return orjson.loads(self.content)
2726

2827

2928
class HttpstanClient:

stan/fit.py

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Generator, Tuple, cast
55

66
import numpy as np
7-
import simdjson
7+
import orjson
88

99

1010
class Fit(collections.abc.Mapping):
@@ -65,43 +65,38 @@ def __init__(
6565
# _draws is an ndarray with shape (num_sample_and_sampler_params + num_flat_params, num_draws, num_chains)
6666
self._draws: np.ndarray
6767

68-
parser = simdjson.Parser()
6968
for chain_index, stan_output in zip(range(self.num_chains), self.stan_outputs):
7069
draw_index = 0
7170
for line in stan_output.splitlines():
7271
try:
73-
msg = cast(simdjson.Object, parser.parse(line))
74-
except ValueError:
75-
# Occurs when draws contain an nan or infinity. simdjson cannot parse such values.
72+
msg = orjson.loads(line)
73+
except orjson.JSONDecodeError:
74+
# Occurs when draws contain a NaN or infinity. orjson cannot parse such values.
7675
msg = json.loads(line)
77-
try:
78-
if msg["topic"] == "sample":
79-
# Ignore sample message which is mixed together with proper draws.
80-
if not isinstance(msg["values"], (simdjson.Object, dict)):
81-
continue
82-
83-
# for the first draw: collect sample and sampler parameter names.
84-
if not hasattr(self, "_draws"):
85-
feature_names = cast(Tuple[str, ...], tuple(msg["values"].keys()))
86-
self.sample_and_sampler_param_names = tuple(
87-
name for name in feature_names if name.endswith("__")
76+
if msg["topic"] == "sample":
77+
# Ignore sample message which is mixed together with proper draws.
78+
if not isinstance(msg["values"], dict):
79+
continue
80+
81+
# for the first draw: collect sample and sampler parameter names.
82+
if not hasattr(self, "_draws"):
83+
feature_names = cast(Tuple[str, ...], tuple(msg["values"].keys()))
84+
self.sample_and_sampler_param_names = tuple(
85+
name for name in feature_names if name.endswith("__")
86+
)
87+
num_rows = len(self.sample_and_sampler_param_names) + num_flat_params
88+
# column-major order ("F") aligns with how the draws are stored (in cols).
89+
self._draws = np.empty((num_rows, num_samples_saved, num_chains), order="F")
90+
# rudimentary check of parameter order (sample & sampler params must be first)
91+
if num_flat_params and feature_names[-1].endswith("__"):
92+
raise RuntimeError(
93+
f"Expected last parameter name to be one declared in program code, found `{feature_names[-1]}`"
8894
)
89-
num_rows = len(self.sample_and_sampler_param_names) + num_flat_params
90-
# column-major order ("F") aligns with how the draws are stored (in cols).
91-
self._draws = np.empty((num_rows, num_samples_saved, num_chains), order="F")
92-
# rudimentary check of parameter order (sample & sampler params must be first)
93-
if num_flat_params and feature_names[-1].endswith("__"):
94-
raise RuntimeError(
95-
f"Expected last parameter name to be one declared in program code, found `{feature_names[-1]}`"
96-
)
97-
98-
draw_row = tuple(msg["values"].values()) # a "row" of values from a single draw from Stan C++
99-
draw_row = cast(Tuple[float, ...], draw_row)
100-
self._draws[:, draw_index, chain_index] = draw_row
101-
draw_index += 1
102-
finally:
103-
# clean up `Object`s produced by parser, required by simdjson
104-
del msg
95+
96+
draw_row = tuple(msg["values"].values()) # a "row" of values from a single draw from Stan C++
97+
draw_row = cast(Tuple[float, ...], draw_row)
98+
self._draws[:, draw_index, chain_index] = draw_row
99+
draw_index += 1
105100
assert draw_index == num_samples_saved
106101
assert self.sample_and_sampler_param_names and self._draws.size
107102
self._draws.flags["WRITEABLE"] = False # type: ignore

stan/model.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import httpstan.services.arguments as arguments
1111
import httpstan.utils
1212
import numpy as np
13-
import simdjson
13+
import orjson
1414
from clikit.io import ConsoleIO
1515

1616
import stan.common
@@ -257,10 +257,10 @@ async def go():
257257

258258
stan_outputs = tuple(stan_outputs) # Fit constructor expects a tuple.
259259

260-
def is_nonempty_logger_message(msg: simdjson.Object):
260+
def is_nonempty_logger_message(msg: dict):
261261
return msg["topic"] == "logger" and msg["values"][0] != "info:" # type: ignore
262262

263-
def is_iteration_or_elapsed_time_logger_message(msg: simdjson.Object):
263+
def is_iteration_or_elapsed_time_logger_message(msg: dict):
264264
# Assumes `msg` is a message with topic `logger`.
265265
text = msg["values"][0] # type: ignore
266266
text = cast(str, text)
@@ -271,19 +271,16 @@ def is_iteration_or_elapsed_time_logger_message(msg: simdjson.Object):
271271
or text.startswith("info:" + " " * 15)
272272
)
273273

274-
parser = simdjson.Parser()
275274
nonstandard_logger_messages = []
276275
for stan_output in stan_outputs:
277276
for line in stan_output.splitlines():
278277
# Do not attempt to parse non-logger messages. Draws could contain nan or inf values.
279-
# simdjson cannot parse lines containing such values.
278+
# orjson cannot parse lines containing such values.
280279
if b'"logger"' not in line:
281280
continue
282-
msg = parser.parse(line)
281+
msg = orjson.loads(line)
283282
if is_nonempty_logger_message(msg) and not is_iteration_or_elapsed_time_logger_message(msg):
284-
nonstandard_logger_messages.append(msg.as_dict())
285-
del msg
286-
del parser # simdjson.Parser is no longer used at this point.
283+
nonstandard_logger_messages.append(msg)
287284

288285
if nonstandard_logger_messages:
289286
io.error_line("<comment>Messages received during sampling:</comment>")

0 commit comments

Comments
 (0)