PineForge HPO 1.11.0
Native hyperparameter optimization for PineForge strategies
Loading...
Searching...
No Matches
warm_binary.py
Go to the documentation of this file.
1"""Dependency-free reference writer for concatenable warm-start v2 blocks."""
2
3from __future__ import annotations
4
5from array import array
6import json
7import math
8import struct
9import sys
10from typing import Any, BinaryIO, Mapping, Sequence
11
12from .continuation import (
13 ContinuationDataContext,
14 WarmStartError,
15 _dump,
16 _loads,
17 _parameters,
18 _real_bits,
19 _validate_data_context,
20 _validate_sampler_checkpoint,
21 space_hash,
22)
23from .study_spec import StudySpec, _finite_parameter_cardinality, _fused_grid_value
24
25MAGIC = b"PFHWARM\0"
26STATE_MAGIC = b"PFHSTATE"
27VERSION = 2
28HEADER = struct.Struct("<8sHHIQQIIII32s")
29NULL_BITS = 0x7FF8000000000000
30STATES = (
31 "ok",
32 "constraint_violation",
33 "engine_error",
34 "objective_error",
35 "constraint_error",
36 "trial_error",
37 "trial_timeout",
38 "pruned",
39 "partial",
40)
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
61 study: StudySpec,
62 trials: Sequence[Mapping[str, Any]],
63 *,
64 sampler_state: str | None = None,
65 symbol_feeds_record: Mapping[str, Any] | None = None,
66 data_context: ContinuationDataContext | None = None,
67) -> bytes:
68 """Encode every attempted trial in one chunk, sorting rows by uint64 trial ID.
69
70 Parameters use lexicographic name order. Missing v0.5 constraint values become
71 canonical nulls; feasibility continues to come from state, exactly as in v0.5.
72 Concatenate independent returned blocks without adding separators or a footer.
73 """
74 try:
75 if not trials:
76 raise ValueError("binary block must contain at least one trial")
77 for trial in trials:
78 identifier = trial["trial_id"]
79 if type(identifier) is not int or not 0 <= identifier < (1 << 64) - 1:
80 raise ValueError("expected uint64 trial_id with a continuation ID")
81 ordered = sorted(trials, key=lambda trial: trial["trial_id"])
82 identifiers = [trial["trial_id"] for trial in ordered]
83 if any(left == right for left, right in zip(identifiers, identifiers[1:])):
84 raise ValueError("duplicate trial_id")
85 parameters = [_parameters(study, trial["parameters"]) for trial in ordered]
86 for trial, values in zip(ordered, parameters):
87 _validate_data_context(trial, data_context, required=True)
88 if data_context is not None:
89 data_context.validate_row(trial, values)
90 states = []
91 for trial in ordered:
92 state = STATES.index(trial["status"])
93 if type(trial["feasible"]) is not bool or trial["feasible"] != (state == 0):
94 raise ValueError("inconsistent status/feasibility/objective")
95 states.append(state)
96 descriptors = bytearray()
97 columns = [_column("Q", identifiers), bytes(states)]
98 for name, parameter in sorted(study.search_space.items()):
99 kind = {"integer": 1, "real": 2, "boolean": 3, "categorical": 4}[
100 parameter.kind
101 ]
102 encoding = 2 if parameter.kind == "real" and parameter.step is None else 1
103 descriptors.extend(struct.pack("<BBH", kind, encoding, 0))
104 values = [row[name] for row in parameters]
105 if encoding == 2:
106 columns.append(_column("d", values))
107 continue
108 count = _finite_parameter_cardinality(parameter, name, [])
109 if count is None or count > 1 << 31:
110 raise ValueError(f"binary parameter grid index exceeds int32: {name}")
111 if parameter.kind == "integer":
112 indices = [
113 (value - parameter.low) // parameter.step for value in values
114 ]
115 elif parameter.kind == "real":
116 indices = [
117 round((value - parameter.low) / parameter.step) for value in values
118 ]
119 for trial, index in zip(ordered, indices):
120 decoded = (
121 float(parameter.low)
122 if parameter.low == parameter.high
123 else _fused_grid_value(index, parameter.step, parameter.low)
124 )
125 if index + 1 == count and decoded > parameter.high:
126 decoded = float(parameter.high)
127 if struct.pack("<d", decoded) != struct.pack(
128 "<d", trial["parameters"][name]
129 ):
130 raise ValueError(f"noncanonical binary grid parameter: {name}")
131 elif parameter.kind == "boolean":
132 indices = [int(value) for value in values]
133 else:
134 indices = [
135 next(
136 index
137 for index, choice in enumerate(parameter.choices)
138 if type(value) is type(choice) and value == choice
139 )
140 for value in values
141 ]
142 for value, index in zip(values, indices):
143 if type(value) is float and struct.pack("<d", value) != struct.pack(
144 "<d", parameter.choices[index]
145 ):
146 raise ValueError(
147 f"noncanonical binary choice parameter: {name}"
148 )
149 if any(not 0 <= index < count for index in indices):
150 raise ValueError(f"invalid binary grid index: {name}")
151 columns.append(_column("i", indices))
152 columns.append(b"".join(_score(trial["objective"]) for trial in ordered))
153 constraint_count = len(study.objective.constraints)
154 constraint_rows = []
155 for trial in ordered:
156 values = trial.get("constraint_values", [None] * constraint_count)
157 if not isinstance(values, (list, tuple)) or len(values) != constraint_count:
158 raise ValueError("constraint column counts do not match study")
159 constraint_rows.append(values)
160 constraint_order = sorted(
161 range(constraint_count),
162 key=lambda index: study.objective.constraints[index],
163 )
164 for column in constraint_order:
165 columns.append(
166 b"".join(_score(values[column]) for values in constraint_rows)
167 )
168 records = (
169 [
170 trial["space"].get("symbol_feeds")
171 for trial in ordered
172 if "space" in trial
173 ]
174 if data_context is None
175 else []
176 )
177 if data_context is not None:
178 symbol_feeds_record = None
179 if symbol_feeds_record is None and records:
180 symbol_feeds_record = records[0]
181 if any(record != symbol_feeds_record for record in records):
182 raise ValueError("symbol feeds differ between trial records")
183 empty_index = (
184 isinstance(study.symbol_feeds, Mapping)
185 and study.symbol_feeds.get("symbols") == {}
186 )
187 if (
188 study.symbol_feeds is not None
189 and not empty_index
190 and symbol_feeds_record is None
191 and data_context is None
192 ):
193 raise ValueError(
194 "symbol feeds record is required for this study's warm header"
195 )
196 runtime = b""
197 if symbol_feeds_record is not None and not isinstance(
198 symbol_feeds_record, Mapping
199 ):
200 raise ValueError("symbol feeds record must be an object")
201 if symbol_feeds_record is not None and symbol_feeds_record.get("symbols") != {}:
202 encoded = json.dumps(
203 symbol_feeds_record,
204 sort_keys=True,
205 separators=(",", ":"),
206 ensure_ascii=False,
207 allow_nan=False,
208 ).encode("utf-8")
209 if len(encoded) > 8 * 1024 * 1024:
210 raise ValueError("symbol feeds header exceeds 8 MiB")
211 runtime = struct.pack("<I", len(encoded)) + encoded
212 context = (
213 b""
214 if data_context is None
215 else struct.pack("<I", 1) + bytes.fromhex(data_context.data_context_sha256)
216 )
217 header_bytes = HEADER.size + len(descriptors) + len(runtime) + len(context)
218 if header_bytes >= 1 << 32:
219 raise ValueError("binary block header length overflow")
220 payload = b"".join(columns)
221 header = HEADER.pack(
222 MAGIC,
223 VERSION,
224 int(bool(runtime)) | (2 if data_context is not None else 0),
225 header_bytes,
226 header_bytes + len(payload),
227 len(ordered),
228 len(study.search_space),
229 1,
230 constraint_count,
231 0,
232 bytes.fromhex(space_hash(study)),
233 )
234 block = header + descriptors + runtime + context + payload
235 if sampler_state is not None:
236 if not isinstance(sampler_state, str) or not sampler_state:
237 raise ValueError("sampler_state must be a nonempty string")
238 state = sampler_state.encode("utf-8")
239 if len(state) > 16 * 1024 * 1024:
240 raise ValueError("sampler_state exceeds 16 MiB")
241 block += STATE_MAGIC + struct.pack("<Q", len(state)) + state
242 return block
243 except (ValueError, TypeError, KeyError, OverflowError, StopIteration) as error:
244 raise WarmStartError(f"warm-start incompatible: {error}") from error
245
246
248 output: BinaryIO,
249 study: StudySpec,
250 trials: Sequence[Mapping[str, Any]],
251 *,
252 sampler_state: str | None = None,
253 symbol_feeds_record: Mapping[str, Any] | None = None,
254 data_context: ContinuationDataContext | None = None,
255) -> int:
256 """Write one validated chunk to a binary stream; return its byte count."""
257 block = encode_warm_block(
258 study,
259 trials,
260 sampler_state=sampler_state,
261 symbol_feeds_record=symbol_feeds_record,
262 data_context=data_context,
263 )
264 written = output.write(block)
265 if written != len(block):
266 raise OSError("short write of binary warm block")
267 return written
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
294 study: StudySpec,
295 content: bytes,
296 *,
297 data_context: ContinuationDataContext | None = None,
298 symbol_feeds_record: Mapping[str, Any] | None = None,
299) -> tuple[dict[str, Any], ...]:
300 """Internal reference reader; public binary preflight still uses the native loader."""
301 try:
302 rows = []
303 offset = 0
304 names = sorted(study.search_space)
305 constraint_order = sorted(
306 range(len(study.objective.constraints)),
307 key=lambda index: study.objective.constraints[index],
308 )
309 while offset < len(content):
310 if content[offset : offset + 8] == STATE_MAGIC:
311 if len(content) - offset < 16:
312 raise ValueError("truncated sampler-state block header")
313 size = struct.unpack_from("<Q", content, offset + 8)[0]
314 if (
315 not 0 < size <= 16 * 1024 * 1024
316 or size > len(content) - offset - 16
317 ):
318 raise ValueError("invalid sampler-state block length")
319 _validate_sampler_checkpoint(
320 content[offset + 16 : offset + 16 + size].decode()
321 )
322 offset += 16 + size
323 continue
324 if len(content) - offset < HEADER.size:
325 raise ValueError("truncated binary block header")
326 (
327 magic,
328 version,
329 flags,
330 header_size,
331 block_size,
332 count,
333 parameter_count,
334 objectives,
335 constraints,
336 reserved,
337 digest,
338 ) = HEADER.unpack_from(content, offset)
339 if magic != MAGIC or version != VERSION or flags & ~3 or reserved:
340 raise ValueError("unsupported binary block magic, version or flags")
341 if digest != bytes.fromhex(space_hash(study)):
342 raise ValueError("search space or objective differs (space_hash)")
343 if (parameter_count, objectives, constraints) != (
344 len(names),
345 1,
346 len(constraint_order),
347 ):
348 raise ValueError("binary column counts do not match study")
349 base_header = HEADER.size + 4 * parameter_count
350 if (
351 not count
352 or not base_header <= header_size <= block_size <= len(content) - offset
353 ):
354 raise ValueError("truncated or invalid binary block length")
355 cursor = base_header
356 feeds = None
357 if flags & 1:
358 if header_size - cursor < 4:
359 raise ValueError("truncated symbol feeds header")
360 size = struct.unpack_from("<I", content, offset + cursor)[0]
361 cursor += 4
362 if not 0 < size <= 8 * 1024 * 1024 or size > header_size - cursor:
363 raise ValueError("invalid symbol feeds header length")
364 feeds = _loads(
365 content[offset + cursor : offset + cursor + size].decode()
366 )
367 cursor += size
368 if flags & 2:
369 if header_size - cursor != 36:
370 raise ValueError("invalid data context header length")
371 context_version = struct.unpack_from("<I", content, offset + cursor)[0]
372 context_digest = content[offset + cursor + 4 : offset + cursor + 36]
373 if (
374 context_version != 1
375 or data_context is None
376 or context_digest != bytes.fromhex(data_context.data_context_sha256)
377 ):
378 raise ValueError("missing or differing data_context_sha256")
379 cursor += 36
380 elif data_context is not None:
381 raise ValueError("bound parent is missing data context identity")
382 if cursor != header_size:
383 raise ValueError("invalid binary block header length")
384 # Context is checked first: its digest already includes the admitted request feeds.
385 if (flags & 1 or data_context is None) and (
386 _feed_identity(feeds) != _feed_identity(symbol_feeds_record)
387 ):
388 raise ValueError(
389 "symbol feeds differ from parent (facts or feed values)"
390 )
391 codecs = []
392 row_size = 17 + 8 * constraints
393 for column, name in enumerate(names):
394 parameter = study.search_space[name]
395 kind, encoding, padding = struct.unpack_from(
396 "<BBH", content, offset + HEADER.size + 4 * column
397 )
398 expected_kind = {
399 "integer": 1,
400 "real": 2,
401 "boolean": 3,
402 "categorical": 4,
403 }[parameter.kind]
404 expected_encoding = (
405 2 if parameter.kind == "real" and parameter.step is None else 1
406 )
407 if (kind, encoding, padding) != (expected_kind, expected_encoding, 0):
408 raise ValueError("binary parameter descriptor differs from study")
409 codecs.append(encoding)
410 row_size += 8 if encoding == 2 else 4
411 if block_size != header_size + count * row_size:
412 raise ValueError("invalid binary payload length")
413 cursor = offset + header_size
414 identifiers = struct.unpack_from(f"<{count}Q", content, cursor)
415 cursor += count * 8
416 states = content[cursor : cursor + count]
417 cursor += count
418 values = [{} for _ in range(count)]
419 for name, encoding in zip(names, codecs):
420 parameter = study.search_space[name]
421 column = struct.unpack_from(
422 f"<{count}{'d' if encoding == 2 else 'i'}", content, cursor
423 )
424 cursor += count * (8 if encoding == 2 else 4)
425 cardinality = _finite_parameter_cardinality(parameter, name, [])
426 for row, value in zip(values, column):
427 if encoding == 1:
428 if cardinality is None or not 0 <= value < cardinality:
429 raise ValueError("invalid binary parameter column")
430 if parameter.kind == "integer":
431 value = parameter.low + value * parameter.step
432 elif parameter.kind == "real":
433 value = (
434 float(parameter.low)
435 if parameter.low == parameter.high
436 else _fused_grid_value(
437 value, parameter.step, parameter.low
438 )
439 )
440 value = min(value, float(parameter.high))
441 elif parameter.kind == "boolean":
442 value = bool(value)
443 else:
444 value = parameter.choices[value]
445 row[name] = value
446
447 def scores() -> tuple[float | None, ...]:
448 nonlocal cursor
449 result = []
450 for index in range(count):
451 bits = struct.unpack_from("<Q", content, cursor + index * 8)[0]
452 value = struct.unpack_from("<d", content, cursor + index * 8)[0]
453 if not math.isfinite(value) and bits != NULL_BITS:
454 raise ValueError("invalid binary objective or constraint value")
455 result.append(None if bits == NULL_BITS else value)
456 cursor += count * 8
457 return tuple(result)
458
459 objectives = scores()
460 constraint_rows = [[None] * constraints for _ in range(count)]
461 for original_column in constraint_order:
462 for row, value in zip(constraint_rows, scores()):
463 row[original_column] = value
464 for identifier, state, parameters, objective, constraint_values in zip(
465 identifiers, states, values, objectives, constraint_rows
466 ):
467 if identifier == (1 << 64) - 1 or state >= len(STATES):
468 raise ValueError("invalid binary trial ID or state")
469 parameters = _parameters(study, parameters)
470 row = {
471 "trial_id": identifier,
472 "status": STATES[state],
473 "feasible": state == 0,
474 "objective": objective,
475 "parameters": parameters,
476 "constraint_values": constraint_values,
477 }
478 if data_context is not None:
479 row.update(data_context.row_identity(parameters))
480 rows.append(row)
481 offset += block_size
482 if not rows:
483 raise ValueError("binary history contains no trials")
484 rows.sort(key=lambda row: row["trial_id"])
485 if any(
486 left["trial_id"] == right["trial_id"] for left, right in zip(rows, rows[1:])
487 ):
488 raise ValueError("duplicate trial_id")
489 return tuple(rows)
490 except (
491 ValueError,
492 TypeError,
493 KeyError,
494 AttributeError,
495 OverflowError,
496 struct.error,
497 ) as error:
498 raise WarmStartError(f"warm-start incompatible: {error}") from error
tuple[dict[str, Any],...] decode_warm_blocks(StudySpec study, bytes content, *ContinuationDataContext|None data_context=None, Mapping[str, Any]|None symbol_feeds_record=None)
bytes encode_warm_block(StudySpec study, Sequence[Mapping[str, Any]] trials, *str|None sampler_state=None, Mapping[str, Any]|None symbol_feeds_record=None, ContinuationDataContext|None data_context=None)
int write_warm_block(BinaryIO output, StudySpec study, Sequence[Mapping[str, Any]] trials, *str|None sampler_state=None, Mapping[str, Any]|None symbol_feeds_record=None, ContinuationDataContext|None data_context=None)