Skip to content

Commit 681b63c

Browse files
feat: Add optional lz4 compression support for arrays passed via base64 or binref encoding (#579)
#### Relevant issue or PR n/a #### Description of changes Adds optional lz4 compression for arrays serialized via `json+base64` and `json+binref` encodings, opt-in through a new `TESSERACT_COMPRESSION` config var (currently `lz4` or unset). lz4 was chosen as the first option because it's a small, single dependency with fast round-trips. Further codecs can be added later without touching the encode/decode call sites. `TESSERACT_COMPRESSION` only controls how a Tesseract *encodes its output*. It has no effect on inputs: each array carries its own `"compression"` field, so a Tesseract decodes any valid input — compressed or not — regardless of config. This means you can pipe a compressed output straight into another Tesseract without configuring the receiver. Key details: - `lz4` is a new runtime dependency for all Tesseract builds. - Arrays are compressed individually rather than compressing the whole buffer, which preserves offset-based random access into binref files. Because compressed length isn't derivable from shape/dtype, the binref buffer spec is extended from `<path>:<offset>` to `<path>:<offset>:<compressed_size>`, and a `"compression"` field is added to the array data dict. The buffer regex and validation pattern are updated accordingly; readers require `compressed_size` when `compression` is set. - `TESSERACT_COMPRESSION` is a runtime config var, so through `tesseract run` it must be forwarded into the container with `-e` (a shell-level prefix stays on the host): ```bash # binref $ tesseract run -e TESSERACT_COMPRESSION=lz4 vectoradd apply -f json+binref -o /tmp/out @inputs.json {"result": {..., "data": {"buffer": "arr.bin:0:35", "encoding": "binref", "compression": "lz4"}}} # base64 $ tesseract run -e TESSERACT_COMPRESSION=lz4 vectoradd apply -f json+base64 @inputs.json {"result": {..., "data": {"buffer": "<base64>", "encoding": "base64", "compression": "lz4"}}} ``` #### Testing done CI. New unit tests cover base64 and binref compress/decompress round-trips, the extended buffer-spec parsing, and the "missing compressed_size" error path; end-to-end `tesseract run` tests exercise `TESSERACT_COMPRESSION=lz4` for both encodings. New benchmark, too. --------- Co-authored-by: Dion Häfner <dion.haefner@simulation.science>
1 parent dd88dfd commit 681b63c

15 files changed

Lines changed: 395 additions & 31 deletions

File tree

benchmarks/test_array_encoding.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,24 @@ class ArrayModel(BaseModel):
3131
data: Array[(None,), Float64]
3232

3333

34-
ENCODINGS = ["json", "base64", "binref"]
34+
ENCODINGS = ["json", "base64", "binref", "base64+lz4", "binref+lz4"]
3535

3636
# Maps short encoding name to the format string used by output_to_bytes
3737
_ENCODING_TO_FORMAT: dict[str, supported_format_type] = {
3838
"json": "json",
3939
"base64": "json+base64",
4040
"binref": "json+binref",
41+
"base64+lz4": "json+base64",
42+
"binref+lz4": "json+binref",
43+
}
44+
45+
# Maps short encoding name to extra kwargs passed to output_to_bytes
46+
_ENCODING_TO_KWARGS: dict[str, dict] = {
47+
"json": {},
48+
"base64": {},
49+
"binref": {},
50+
"base64+lz4": {"compression": "lz4"},
51+
"binref+lz4": {"compression": "lz4"},
4152
}
4253

4354

@@ -78,44 +89,50 @@ def test_encoding(benchmark, encoding_and_size):
7889
encoding, size = encoding_and_size
7990
model = ArrayModel(data=create_test_array(size))
8091
fmt = _ENCODING_TO_FORMAT[encoding]
92+
extra_kwargs = _ENCODING_TO_KWARGS[encoding]
93+
uses_binref = "binref" in encoding
8194

8295
with tempfile.TemporaryDirectory() as tmpdir:
83-
if encoding == "binref":
96+
if uses_binref:
8497

8598
def setup():
8699
_clear_dir(tmpdir)
87100

88101
benchmark.pedantic(
89102
output_to_bytes,
90103
args=(model, fmt),
91-
kwargs={"base_dir": tmpdir},
104+
kwargs={"base_dir": tmpdir, **extra_kwargs},
92105
setup=setup,
93106
rounds=_binref_rounds(size),
94107
)
95108
else:
96-
benchmark(output_to_bytes, model, fmt)
109+
benchmark(output_to_bytes, model, fmt, **extra_kwargs)
97110

98111

99112
def test_decoding(benchmark, encoding_and_size):
100113
encoding, size = encoding_and_size
101114
model = ArrayModel(data=create_test_array(size))
102115
fmt = _ENCODING_TO_FORMAT[encoding]
116+
extra_kwargs = _ENCODING_TO_KWARGS[encoding]
117+
uses_binref = "binref" in encoding
103118

104119
with tempfile.TemporaryDirectory() as tmpdir:
105120
ctx: dict[str, str] = {}
106-
if encoding == "binref":
121+
if uses_binref:
107122
ctx["base_dir"] = tmpdir
108123

109-
encoded = output_to_bytes(model, fmt, base_dir=tmpdir)
124+
encoded = output_to_bytes(model, fmt, base_dir=tmpdir, **extra_kwargs)
110125

111-
if encoding == "binref":
126+
if uses_binref:
112127
# binref filenames are random UUIDs, so we must re-encode in setup
113128
# and pass the fresh payload to the decode call via a mutable wrapper.
114129
payload = [encoded]
115130

116131
def setup():
117132
_clear_dir(tmpdir)
118-
payload[0] = output_to_bytes(model, fmt, base_dir=tmpdir)
133+
payload[0] = output_to_bytes(
134+
model, fmt, base_dir=tmpdir, **extra_kwargs
135+
)
119136

120137
def decode():
121138
ArrayModel.model_validate_json(payload[0], context=ctx)
@@ -129,17 +146,19 @@ def test_roundtrip(benchmark, encoding_and_size):
129146
encoding, size = encoding_and_size
130147
model = ArrayModel(data=create_test_array(size))
131148
fmt = _ENCODING_TO_FORMAT[encoding]
149+
extra_kwargs = _ENCODING_TO_KWARGS[encoding]
150+
uses_binref = "binref" in encoding
132151

133152
with tempfile.TemporaryDirectory() as tmpdir:
134153
ctx: dict[str, str] = {}
135-
if encoding == "binref":
154+
if uses_binref:
136155
ctx["base_dir"] = tmpdir
137156

138157
def roundtrip():
139-
enc = output_to_bytes(model, fmt, base_dir=tmpdir)
158+
enc = output_to_bytes(model, fmt, base_dir=tmpdir, **extra_kwargs)
140159
ArrayModel.model_validate_json(enc, context=ctx)
141160

142-
if encoding == "binref":
161+
if uses_binref:
143162

144163
def setup():
145164
_clear_dir(tmpdir)

docs/content/using-tesseracts/array-encodings.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,19 @@ $ curl \
140140
The `.bin` file references are relative to the `--output-path`.
141141
:::
142142
::::
143+
144+
### lz4 compression
145+
146+
Set the `TESSERACT_COMPRESSION=lz4` config variable to compress arrays in the output. This applies to both `json+binref` and `json+base64` formats. For binref, each array is compressed individually, preserving offset-based random access, with the compressed size embedded in the buffer path (`<file>:<offset>:<compressed_size>`).
147+
148+
`TESSERACT_COMPRESSION` only affects how a Tesseract encodes its _output_; inputs are always decoded according to their own per-array `compression` field, so no configuration is needed to read compressed data.
149+
150+
Because it is a runtime config variable, it must be forwarded into the container with `-e` when using `tesseract run` (a plain shell prefix would stay on the host):
151+
152+
```bash
153+
$ tesseract run -e TESSERACT_COMPRESSION=lz4 vectoradd apply -f "json+binref" -o /tmp/output @examples/vectoradd/example_inputs.json
154+
$ cat /tmp/output/results.json
155+
{"result":{"object_type":"array","shape":[3],"dtype":"float64","data":{"buffer":"....bin:0:35","encoding":"binref","compression":"lz4"}}}
156+
```
157+
158+
When calling `tesseract-runtime` directly (e.g. inside a container), the plain `TESSERACT_COMPRESSION=lz4 tesseract-runtime ...` prefix works as usual.

0 commit comments

Comments
 (0)