Skip to content

Commit 83ae087

Browse files
ItsMrLinfacebook-github-bot
authored andcommitted
LILO hash computation and stamping for data freshness (#4992)
Summary: Add hash-based data freshness tracking for LILO (Language-in-the-Loop) pairwise preference labels. When LILOPairwiseMetric produces labels, it now stamps a SHA-256 hash of the experiment's LILO inputs (metric data for input_metric_names + LLM messages) onto the trial's _properties. If any of these inputs change (new data arrives, data is updated, or the user modifies LLM messages), the hash changes, indicating that existing LILO labels are stale. Changes: - Add `LILO_INPUT_HASH` key to `Keys` enum in `constants.py` - Create `ax/utils/common/hash_utils.py` with `compute_lilo_input_hash` (standalone hash function) and `get_current_lilo_hash` (convenience helper that looks up the pairwise `DerivedMetric` on an experiment, extracts `input_metric_names`, and computes the hash — returns `None` if no pairwise metric is registered) - Stamp hash in `LILOPairwiseMetric._compute_derived_values` after producing labels - Add tests for hash determinism, sensitivity to data/message changes, stamping, and `get_current_lilo_hash` helper Reviewed By: saitcakmak Differential Revision: D95284287
1 parent 3e8e4e7 commit 83ae087

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

ax/utils/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class Keys(StrEnum):
6464
FRAC_RANDOM = "frac_random"
6565
FULL_PARAMETERIZATION = "full_parameterization"
6666
IMMUTABLE_SEARCH_SPACE_AND_OPT_CONF = "immutable_search_space_and_opt_config"
67+
LILO_INPUT_HASH = "lilo_input_hash"
6768
LILO_LABELING = "lilo_labeling"
6869
LLM_MESSAGES = "llm_messages"
6970
LONG_RUN = "long_run"

ax/utils/common/hash_utils.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
9+
"""Hash utilities for LILO (Language-in-the-Loop) data freshness tracking."""
10+
11+
from __future__ import annotations
12+
13+
import hashlib
14+
from typing import TYPE_CHECKING
15+
16+
from ax.core.derived_metric import DerivedMetric
17+
from ax.utils.common.constants import Keys
18+
19+
if TYPE_CHECKING:
20+
from ax.core.experiment import Experiment
21+
22+
23+
def compute_lilo_input_hash(
24+
experiment: Experiment,
25+
input_metric_names: list[str],
26+
) -> str:
27+
"""Compute a hash of the experiment state relevant to LILO labeling.
28+
29+
The hash captures two components:
30+
1. The experiment's LLM messages (user preferences that guide labeling).
31+
2. The observed metric data for ``input_metric_names`` across all trials.
32+
33+
If any of these inputs change, the hash changes, indicating that existing
34+
LILO labels are stale and should be excluded from model fitting.
35+
36+
Args:
37+
experiment: The experiment whose state to hash.
38+
input_metric_names: Names of the base metrics whose observed values
39+
are shown to the LLM for pairwise comparison.
40+
41+
Returns:
42+
An SHA-256 hex digest string representing the current LILO input state.
43+
"""
44+
parts: list[str] = []
45+
46+
# Component 1: LLM messages (canonical serialization).
47+
for msg in experiment.llm_messages:
48+
parts.append(f"{msg.role}:{msg.content}")
49+
50+
parts.append("---") # Separator between components.
51+
52+
# Component 2: Metric data for input_metric_names.
53+
data = experiment.data
54+
if not data.empty:
55+
df = data.df
56+
metric_df = df[df["metric_name"].isin(input_metric_names)]
57+
if not metric_df.empty:
58+
# Sort deterministically and serialize key columns.
59+
sorted_df = metric_df.sort_values(
60+
["trial_index", "arm_name", "metric_name"]
61+
)
62+
for _, row in sorted_df.iterrows():
63+
parts.append(
64+
f"{row['trial_index']}|{row['arm_name']}|"
65+
f"{row['metric_name']}|{row['mean']}|{row['sem']}"
66+
)
67+
68+
content = "\n".join(parts)
69+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
70+
71+
72+
def get_current_lilo_hash(experiment: Experiment) -> str | None:
73+
"""Compute the current LILO input hash, or ``None`` if not applicable.
74+
75+
Looks up the pairwise preference metric on the experiment by name
76+
(``Keys.PAIRWISE_PREFERENCE_QUERY``), checks that it is a
77+
``DerivedMetric`` (which provides ``input_metric_names``), and computes
78+
the hash. In practice only ``LILOPairwiseMetric`` satisfies both
79+
conditions; we check ``DerivedMetric`` rather than ``LILOPairwiseMetric``
80+
directly because the latter lives in ``ax.fb`` and cannot be imported
81+
from this OSS module without creating a circular dependency.
82+
83+
Returns:
84+
The SHA-256 hex digest of the current LILO input state, or ``None``
85+
if no suitable pairwise ``DerivedMetric`` is registered.
86+
"""
87+
pairwise_metric_name = Keys.PAIRWISE_PREFERENCE_QUERY.value
88+
metric = experiment.metrics.get(pairwise_metric_name)
89+
# TODO: Replace `DerivedMetric` with `LILOPairwiseMetric` here.
90+
if metric is None or not isinstance(metric, DerivedMetric):
91+
return None
92+
return compute_lilo_input_hash(
93+
experiment=experiment,
94+
input_metric_names=metric.input_metric_names,
95+
)

0 commit comments

Comments
 (0)