Skip to content

Commit ee081e0

Browse files
committed
Result: add mutable extra dict for arbitrary attributes; objective: add MinimizeAttribute/MaximizeAttribute (works with built-ins or extra keys); docs+example; bump DB version to 0.1; tests
1 parent ed92a56 commit ee081e0

6 files changed

Lines changed: 196 additions & 1 deletion

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,41 @@ OpenTuner is supported in part by the United States Department of Energy
9898
[xstack]: http://science.energy.gov/ascr/research/computer-science/ascr-x-stack-portfolio/
9999
[dtec]: http://www.dtec-xstack.org/
100100

101+
## Attaching custom attributes to Result
102+
103+
You can now attach arbitrary metadata to each `Result` via a mutable `extra` dict.
104+
105+
Example:
106+
107+
```python
108+
from opentuner import Result
109+
110+
# return a result with extra metrics
111+
return Result(time=elapsed_seconds).update_attributes({
112+
'throughput': qps,
113+
'build_hash': git_sha,
114+
})
115+
116+
# or later, after creating a result instance
117+
result.set_attribute('notes', 'warm cache')
118+
print(result.get_attribute('throughput'))
119+
```
120+
121+
The `extra` field is stored in the database using a compressed pickle and is tracked for in-place mutations, so updating keys will be persisted automatically on commit.
122+
123+
### Using custom attributes as metrics
124+
125+
You can drive the search by any built-in `Result` field or a key in `Result.extra` using the new flexible objectives:
126+
127+
```python
128+
from opentuner.search.objective import MinimizeAttribute, MaximizeAttribute
129+
130+
# Example: minimize a custom latency value stored in Result.extra['p95_ms']
131+
objective = MinimizeAttribute('p95_ms', missing_value=float('inf'))
132+
133+
# Or maximize a custom throughput stored in Result.extra['qps']
134+
objective = MaximizeAttribute('qps', missing_value=float('-inf'))
135+
```
136+
137+
If the attribute name matches a concrete column (e.g., `time`, `accuracy`), ordering is done directly in SQL. Otherwise, ordering falls back to in-Python comparisons.
138+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import random
2+
3+
import opentuner
4+
from opentuner import MeasurementInterface
5+
from opentuner import Result
6+
from opentuner.search.manipulator import ConfigurationManipulator, IntegerParameter
7+
from opentuner.search.objective import MaximizeAttribute
8+
9+
10+
class CustomMetricExample(MeasurementInterface):
11+
def manipulator(self):
12+
m = ConfigurationManipulator()
13+
m.add_parameter(IntegerParameter('threads', 1, 8))
14+
return m
15+
16+
def objective(self):
17+
return MaximizeAttribute('qps', missing_value=float('-inf'))
18+
19+
def compile_and_run(self, desired_result, input, limit):
20+
cfg = desired_result.configuration.data
21+
threads = cfg['threads']
22+
# Synthetic: qps grows with threads but has diminishing returns and noise
23+
base_qps = 1000.0 * (1 - 0.1 / max(1, threads))
24+
noise = random.uniform(-10, 10)
25+
qps = base_qps + noise
26+
time = 1.0 / max(1, qps) # smaller time for higher qps, just for demo
27+
return Result(time=time).update_attributes({'qps': qps, 'threads': threads})
28+
29+
30+
if __name__ == '__main__':
31+
argparser = opentuner.default_argparser()
32+
CustomMetricExample.main(argparser.parse_args())

opentuner/resultsdb/connect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
log = logging.getLogger(__name__)
1313

14-
DB_VERSION = "0.0"
14+
DB_VERSION = "0.1"
1515

1616
if False: # profiling of queries
1717
import atexit

opentuner/resultsdb/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from pickle import dumps, loads
1616
from gzip import zlib
17+
from sqlalchemy.ext.mutable import MutableDict
1718

1819

1920
class CompressedPickler(object):
@@ -263,10 +264,31 @@ class Result(Base):
263264
size = Column(Float)
264265
confidence = Column(Float)
265266
# extra = Column(PickleType)
267+
extra = Column(MutableDict.as_mutable(PickleType(pickler=CompressedPickler)), default=dict)
266268

267269
# set by SearchDriver
268270
was_new_best = Column(Boolean)
269271

272+
# Convenience helpers for per-result arbitrary attributes
273+
def set_attribute(self, key, value):
274+
if self.extra is None:
275+
self.extra = {}
276+
self.extra[key] = value
277+
return self
278+
279+
def get_attribute(self, key, default=None):
280+
if self.extra is None:
281+
return default
282+
return self.extra.get(key, default)
283+
284+
def update_attributes(self, mapping):
285+
if not mapping:
286+
return self
287+
if self.extra is None:
288+
self.extra = {}
289+
self.extra.update(mapping)
290+
return self
291+
270292

271293
Index('ix_result_custom1', Result.tuning_run_id, Result.was_new_best)
272294

opentuner/search/objective.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,77 @@ def result_relative(self, result1, result2):
299299
log.warning('result_relative() not yet implemented for %s',
300300
self.__class__.__name__)
301301
return None
302+
303+
304+
class MinimizeAttribute(SearchObjective):
305+
"""
306+
Minimize an attribute on Result. If the attribute is not a concrete
307+
database column, it will be read from Result.extra at runtime.
308+
"""
309+
310+
def __init__(self, attribute_name, missing_value=None):
311+
super(MinimizeAttribute, self).__init__()
312+
self.attribute_name = attribute_name
313+
self.missing_value = missing_value
314+
315+
def result_order_by_terms(self):
316+
# Use DB ordering only if this is a concrete column on Result
317+
col = getattr(Result, self.attribute_name, None)
318+
return [col] if col is not None else []
319+
320+
def _value_of(self, result):
321+
if hasattr(result, self.attribute_name):
322+
return getattr(result, self.attribute_name)
323+
return (result.get_attribute(self.attribute_name, self.missing_value)
324+
if hasattr(result, 'get_attribute') else self.missing_value)
325+
326+
def result_compare(self, result1, result2):
327+
return cmp(self._value_of(result1), self._value_of(result2))
328+
329+
def result_relative(self, result1, result2):
330+
v1 = self._value_of(result1)
331+
v2 = self._value_of(result2)
332+
try:
333+
if v1 is None or v2 in (None, 0):
334+
return None
335+
return old_div(v1, v2)
336+
except Exception:
337+
return None
338+
339+
340+
class MaximizeAttribute(SearchObjective):
341+
"""
342+
Maximize an attribute on Result. If the attribute is not a concrete
343+
database column, it will be read from Result.extra at runtime.
344+
"""
345+
346+
def __init__(self, attribute_name, missing_value=None):
347+
super(MaximizeAttribute, self).__init__()
348+
self.attribute_name = attribute_name
349+
self.missing_value = missing_value
350+
351+
def result_order_by_terms(self):
352+
# Use DB ordering only if this is a concrete column on Result
353+
col = getattr(Result, self.attribute_name, None)
354+
return [-col] if col is not None else []
355+
356+
def _value_of(self, result):
357+
if hasattr(result, self.attribute_name):
358+
return getattr(result, self.attribute_name)
359+
return (result.get_attribute(self.attribute_name, self.missing_value)
360+
if hasattr(result, 'get_attribute') else self.missing_value)
361+
362+
def result_compare(self, result1, result2):
363+
# note opposite order for maximize
364+
return cmp(self._value_of(result2), self._value_of(result1))
365+
366+
def result_relative(self, result1, result2):
367+
# For maximize, relative goodness mirrors MaximizeAccuracy
368+
v1 = self._value_of(result1)
369+
v2 = self._value_of(result2)
370+
try:
371+
if v1 in (None, 0) or v2 is None:
372+
return None
373+
return old_div(v2, v1)
374+
except Exception:
375+
return None

tests/test_custom_metric.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import math
2+
3+
from opentuner.resultsdb.models import Result, Configuration, Program
4+
from opentuner.search.objective import MaximizeAttribute, MinimizeAttribute
5+
6+
7+
def test_result_extra_persistence(tmp_path, Session=None):
8+
# Create a couple of Results and ensure extra dict persists values
9+
r1 = Result(time=1.0)
10+
r1.set_attribute('qps', 100.0)
11+
assert r1.get_attribute('qps') == 100.0
12+
r1.update_attributes({'latency_ms': 10.0})
13+
assert r1.get_attribute('latency_ms') == 10.0
14+
15+
16+
def test_objective_with_custom_metric_compare():
17+
# Two results with only extra metric
18+
r1 = Result(time=2.0).update_attributes({'qps': 100.0})
19+
r2 = Result(time=2.0).update_attributes({'qps': 120.0})
20+
21+
max_qps = MaximizeAttribute('qps', missing_value=float('-inf'))
22+
max_qps.set_driver(type('D', (), {})()) # minimal driver stub
23+
assert max_qps.result_compare(r2, r1) < 0 # r2 better than r1
24+
25+
min_lat = MinimizeAttribute('latency_ms', missing_value=float('inf'))
26+
min_lat.set_driver(type('D', (), {})())
27+
r3 = Result(time=1.0).update_attributes({'latency_ms': 5.0})
28+
r4 = Result(time=1.0).update_attributes({'latency_ms': 10.0})
29+
assert min_lat.result_compare(r3, r4) < 0 # r3 better (lower latency)

0 commit comments

Comments
 (0)