Skip to content

Commit dadd88e

Browse files
authored
Add size_on_disk API to OliveModelHandler and corresponding metric (microsoft#2262)
## Add size_on_disk API to OliveModelHandler and corresponding metric ## Checklist before requesting a review - [ ] Add unit tests for this change. - [x] Make sure all tests can pass. - [ ] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link
1 parent e9356d9 commit dadd88e

File tree

12 files changed

+172
-36
lines changed

12 files changed

+172
-36
lines changed

olive/engine/engine.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -459,21 +459,29 @@ def _create_pareto_frontier_footprint(self, artifacts_dir: Path):
459459
)
460460

461461
def _dump_run_history(self, run_history, output_path: Path):
462-
if not run_history:
463-
logger.info("No run history to dump!")
464-
return
465-
headers = run_history[0]._fields
466-
try:
467-
from tabulate import tabulate
462+
from olive.logging import get_verbosity, set_verbosity
468463

469-
formatted_rls = tabulate([tuple(rh) for rh in run_history], headers=headers, tablefmt="grid")
470-
logger.info("run history:\n%s", formatted_rls)
471-
except ImportError:
472-
logger.info("Please install tabulate for better run history output")
473-
formatted_rls = run_history
474-
if not self.skip_saving_artifacts:
475-
with Path(output_path).open("w") as f:
476-
f.write(f"{formatted_rls}")
464+
def _dump_run_history_internal():
465+
if not run_history:
466+
logger.info("No run history to dump!")
467+
return
468+
headers = run_history[0]._fields
469+
try:
470+
from tabulate import tabulate
471+
472+
formatted_rls = tabulate([tuple(rh) for rh in run_history], headers=headers, tablefmt="grid")
473+
logger.info("run history:\n%s", formatted_rls)
474+
except ImportError:
475+
logger.info("Please install tabulate for better run history output")
476+
formatted_rls = run_history
477+
if not self.skip_saving_artifacts:
478+
with Path(output_path).open("w") as f:
479+
f.write(f"{formatted_rls}")
480+
481+
verbosity = get_verbosity()
482+
set_verbosity(logging.INFO)
483+
_dump_run_history_internal()
484+
set_verbosity(verbosity)
477485

478486
def resolve_objectives(
479487
self,

olive/evaluator/metric.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
from olive.common.utils import StrEnumBase
1111
from olive.data.config import DataConfig
1212
from olive.evaluator.accuracy import AccuracyBase
13-
from olive.evaluator.metric_config import LatencyMetricConfig, MetricGoal, ThroughputMetricConfig, get_user_config_class
13+
from olive.evaluator.metric_config import (
14+
LatencyMetricConfig,
15+
MetricGoal,
16+
SizeOnDiskMetricConfig,
17+
ThroughputMetricConfig,
18+
get_user_config_class,
19+
)
1420

1521
logger = logging.getLogger(__name__)
1622

@@ -20,6 +26,7 @@ class MetricType(StrEnumBase):
2026
ACCURACY = "accuracy"
2127
LATENCY = "latency"
2228
THROUGHPUT = "throughput"
29+
SIZE_ON_DISK = "size_on_disk"
2330
CUSTOM = "custom"
2431

2532

@@ -58,6 +65,10 @@ class ThroughputSubType(StrEnumBase):
5865
P999 = "p999"
5966

6067

68+
class SizeOnDiskSubType(StrEnumBase):
69+
BYTES = "bytes"
70+
71+
6172
class SubMetric(ConfigBase):
6273
name: Union[AccuracySubType, LatencyMetricConfig, str]
6374
metric_config: ConfigBase = None
@@ -158,6 +169,8 @@ def validate_sub_types(cls, v, values):
158169
sub_metric_type_cls = LatencySubType
159170
elif values["type"] == MetricType.THROUGHPUT:
160171
sub_metric_type_cls = ThroughputSubType
172+
elif values["type"] == MetricType.SIZE_ON_DISK:
173+
sub_metric_type_cls = SizeOnDiskSubType
161174
# if not exist, will raise ValueError
162175
v["name"] = sub_metric_type_cls(v["name"])
163176
except ValueError:
@@ -182,6 +195,9 @@ def validate_sub_types(cls, v, values):
182195
elif values["type"] == MetricType.THROUGHPUT:
183196
v["higher_is_better"] = v.get("higher_is_better", True)
184197
metric_config_cls = ThroughputMetricConfig
198+
elif values["type"] == MetricType.SIZE_ON_DISK:
199+
v["higher_is_better"] = False
200+
metric_config_cls = SizeOnDiskMetricConfig
185201
v["metric_config"] = validate_config(v.get("metric_config", {}), metric_config_cls)
186202

187203
return v

olive/evaluator/metric_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class ThroughputMetricConfig(ConfigBase):
5959
sleep_num: int = SLEEP_NUM
6060

6161

62+
class SizeOnDiskMetricConfig(ConfigBase):
63+
pass
64+
65+
6266
class MetricGoal(ConfigBase):
6367
type: str # threshold , deviation, percent-deviation
6468
value: float

olive/evaluator/olive_evaluator.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@
2525
from olive.data.config import DataConfig
2626
from olive.data.container.dummy_data_container import TRANSFORMER_DUMMY_DATA_CONTAINER
2727
from olive.data.template import dummy_data_config_template
28-
from olive.evaluator.metric import LatencySubType, Metric, MetricType, ThroughputSubType, get_latency_config_from_metric
28+
from olive.evaluator.metric import (
29+
LatencySubType,
30+
Metric,
31+
MetricType,
32+
SizeOnDiskSubType,
33+
ThroughputSubType,
34+
get_latency_config_from_metric,
35+
)
2936
from olive.evaluator.metric_backend import MetricBackend
3037
from olive.evaluator.metric_result import MetricResult, SubMetricResult, flatten_metric_result, joint_metric_key
3138
from olive.evaluator.registry import Registry
@@ -276,6 +283,19 @@ def _evaluate_throughput(
276283
latencies = self._evaluate_raw_latency(model, metric, dataloader, post_func, device, execution_providers)
277284
return OliveEvaluator.compute_throughput(metric, latencies)
278285

286+
def _evaluate_size_on_disk(
287+
self,
288+
model: "OliveModelHandler",
289+
metric: Metric,
290+
dataloader: "DataLoader",
291+
post_func=None,
292+
device: Device = Device.CPU,
293+
execution_providers: Union[str, list[str]] = None,
294+
) -> MetricResult:
295+
return MetricResult.parse_obj(
296+
{SizeOnDiskSubType.BYTES.value: {"value": model.size_on_disk, "priority": -1, "higher_is_better": False}}
297+
)
298+
279299
def _evaluate_custom(
280300
self,
281301
model: "OliveModelHandler",
@@ -335,6 +355,10 @@ def evaluate(
335355
metrics_res[metric.name] = self._evaluate_throughput(
336356
model, metric, dataloader, post_func, device, execution_providers
337357
)
358+
elif metric.type == MetricType.SIZE_ON_DISK:
359+
metrics_res[metric.name] = self._evaluate_size_on_disk(
360+
model, metric, dataloader, post_func, device, execution_providers
361+
)
338362
elif metric.type == MetricType.CUSTOM:
339363
metrics_res[metric.name] = self._evaluate_custom(
340364
model, metric, dataloader, eval_func, post_func, device, execution_providers
@@ -1056,30 +1080,44 @@ def evaluate(
10561080
self.model_class,
10571081
{k: v for k, v in init_args.items() if k in ["device", "ep", "ep_options"]},
10581082
)
1059-
lmmodel = get_model(self.model_class)(**init_args, batch_size=self.batch_size, max_length=self.max_length)
1060-
1061-
results = simple_evaluate(
1062-
model=lmmodel,
1063-
tasks=self.tasks,
1064-
task_manager=TaskManager(),
1065-
log_samples=False,
1066-
batch_size=self.batch_size,
1067-
device=device,
1068-
limit=self.limit,
1069-
)
10701083

10711084
metrics = {}
1072-
for task_name in sorted(results["results"].keys()):
1073-
metric_items = sorted(results["results"][task_name].items())
1085+
if MetricType.SIZE_ON_DISK.value in self.tasks:
1086+
self.tasks.remove(MetricType.SIZE_ON_DISK.value)
1087+
metrics[MetricType.SIZE_ON_DISK.value] = MetricResult.parse_obj(
1088+
{
1089+
SizeOnDiskSubType.BYTES.value: {
1090+
"value": model.size_on_disk,
1091+
"priority": -1,
1092+
"higher_is_better": False,
1093+
}
1094+
}
1095+
)
1096+
1097+
if self.tasks:
1098+
lmmodel = get_model(self.model_class)(**init_args, batch_size=self.batch_size, max_length=self.max_length)
1099+
1100+
results = simple_evaluate(
1101+
model=lmmodel,
1102+
tasks=self.tasks,
1103+
task_manager=TaskManager(),
1104+
log_samples=False,
1105+
batch_size=self.batch_size,
1106+
device=device,
1107+
limit=self.limit,
1108+
)
1109+
1110+
for task_name in sorted(results["results"].keys()):
1111+
metric_items = sorted(results["results"][task_name].items())
10741112

1075-
task_metrics = {}
1076-
for mf, v in metric_items:
1077-
if mf != "alias":
1078-
m, _ = mf.split(",", 1)
1079-
if not m.endswith("_stderr"):
1080-
task_metrics[m] = SubMetricResult(value=v, priority=-1, higher_is_better=True)
1113+
task_metrics = {}
1114+
for mf, v in metric_items:
1115+
if mf != "alias":
1116+
m, _ = mf.split(",", 1)
1117+
if not m.endswith("_stderr"):
1118+
task_metrics[m] = SubMetricResult(value=v, priority=-1, higher_is_better=True)
10811119

1082-
metrics[task_name] = MetricResult.parse_obj(task_metrics)
1120+
metrics[task_name] = MetricResult.parse_obj(task_metrics)
10831121

10841122
return flatten_metric_result(metrics)
10851123

olive/model/handler/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ def model_path(self) -> str:
6363
"""Return local model path."""
6464
return self.get_resource("model_path")
6565

66+
@property
67+
@abstractmethod
68+
def size_on_disk(self) -> int:
69+
"""Compute size of the model on disk."""
70+
raise NotImplementedError
71+
6672
@abstractmethod
6773
def load_model(self, rank: int = None, cache_model: bool = True) -> object:
6874
"""Load model from disk, return in-memory model object.

olive/model/handler/composite.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ def get_model_components(self) -> list[tuple[str, OliveModelHandler]]:
8080
def load_model(self, rank: int = None, cache_model: bool = True):
8181
raise NotImplementedError
8282

83+
@property
84+
def size_on_disk(self) -> int:
85+
"""Compute size of the model on disk."""
86+
raise NotImplementedError
87+
8388
def prepare_session(
8489
self,
8590
inference_settings: Optional[dict[str, Any]] = None,

olive/model/handler/hf.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ def load_model(self, rank: int = None, cache_model: bool = True) -> HfModelHandl
188188
model_attributes=self.model_attributes,
189189
)
190190

191+
@property
192+
def size_on_disk(self) -> int:
193+
"""Compute size of the model on disk."""
194+
nbytes = 0
195+
for rank in range(self.num_ranks):
196+
model = self.load_model(rank, cache_model=False)
197+
nbytes += model.size_on_disk
198+
return nbytes
199+
191200
def prepare_session(
192201
self,
193202
inference_settings: Optional[dict[str, Any]] = None,

olive/model/handler/onnx.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ def model_path(self) -> str:
7676
model_path = super().model_path
7777
return get_onnx_file_path(model_path, self.onnx_file_name) if model_path else None
7878

79+
@property
80+
def size_on_disk(self) -> int:
81+
"""Compute size of the model on disk."""
82+
model = self.load_model()
83+
return model.ByteSize()
84+
7985
@property
8086
def external_initializers_path(self) -> Optional[str]:
8187
model_path = super().model_path
@@ -245,6 +251,15 @@ def load_model(self, rank: int = None, cache_model: bool = True) -> ONNXModelHan
245251
model_attributes=self.model_attributes,
246252
)
247253

254+
@property
255+
def size_on_disk(self) -> int:
256+
"""Compute size of the model on disk."""
257+
nbytes = 0
258+
for rank in range(self.num_ranks):
259+
model = self.load_model(rank, cache_model=False)
260+
nbytes += model.ByteSize()
261+
return nbytes
262+
248263
def prepare_session(
249264
self,
250265
inference_settings: Optional[dict[str, Any]] = None,

olive/model/handler/openvino.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ def load_model(self, rank: int = None, cache_model: bool = True):
6161
core = ov.Core()
6262
return core.read_model(self.model_config["model"])
6363

64+
@property
65+
def size_on_disk(self) -> int:
66+
"""Compute size of the model on disk."""
67+
raise NotImplementedError
68+
6469
def prepare_session(
6570
self,
6671
inference_settings: Optional[dict[str, Any]] = None,

olive/model/handler/pytorch.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ def __init__(
4040
io_config=io_config,
4141
)
4242

43+
@property
44+
def size_on_disk(self) -> int:
45+
"""Compute size of the model on disk."""
46+
import torch
47+
48+
class ByteCounter:
49+
def __init__(self):
50+
self.nbytes = 0
51+
52+
def write(self, data):
53+
self.nbytes += len(data)
54+
55+
def flush(self):
56+
pass
57+
58+
counter = ByteCounter()
59+
model = self.load_model()
60+
torch.save(model.state_dict(), counter)
61+
return counter.nbytes
62+
4363
def prepare_session(
4464
self,
4565
inference_settings: Optional[dict[str, Any]] = None,

0 commit comments

Comments
 (0)