triton-model-analyzer 1.48.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_analyzer/__init__.py +15 -0
- model_analyzer/analyzer.py +448 -0
- model_analyzer/cli/__init__.py +15 -0
- model_analyzer/cli/cli.py +193 -0
- model_analyzer/config/__init__.py +15 -0
- model_analyzer/config/generate/__init__.py +15 -0
- model_analyzer/config/generate/automatic_model_config_generator.py +164 -0
- model_analyzer/config/generate/base_model_config_generator.py +352 -0
- model_analyzer/config/generate/brute_plus_binary_parameter_search_run_config_generator.py +164 -0
- model_analyzer/config/generate/brute_run_config_generator.py +154 -0
- model_analyzer/config/generate/concurrency_sweeper.py +75 -0
- model_analyzer/config/generate/config_generator_interface.py +52 -0
- model_analyzer/config/generate/coordinate.py +143 -0
- model_analyzer/config/generate/coordinate_data.py +86 -0
- model_analyzer/config/generate/generator_utils.py +116 -0
- model_analyzer/config/generate/manual_model_config_generator.py +187 -0
- model_analyzer/config/generate/model_config_generator_factory.py +92 -0
- model_analyzer/config/generate/model_profile_spec.py +74 -0
- model_analyzer/config/generate/model_run_config_generator.py +154 -0
- model_analyzer/config/generate/model_variant_name_manager.py +150 -0
- model_analyzer/config/generate/neighborhood.py +536 -0
- model_analyzer/config/generate/optuna_plus_concurrency_sweep_run_config_generator.py +141 -0
- model_analyzer/config/generate/optuna_run_config_generator.py +838 -0
- model_analyzer/config/generate/perf_analyzer_config_generator.py +312 -0
- model_analyzer/config/generate/quick_plus_concurrency_sweep_run_config_generator.py +130 -0
- model_analyzer/config/generate/quick_run_config_generator.py +753 -0
- model_analyzer/config/generate/run_config_generator_factory.py +329 -0
- model_analyzer/config/generate/search_config.py +112 -0
- model_analyzer/config/generate/search_dimension.py +73 -0
- model_analyzer/config/generate/search_dimensions.py +85 -0
- model_analyzer/config/generate/search_parameter.py +49 -0
- model_analyzer/config/generate/search_parameters.py +388 -0
- model_analyzer/config/input/__init__.py +15 -0
- model_analyzer/config/input/config_command.py +483 -0
- model_analyzer/config/input/config_command_profile.py +1747 -0
- model_analyzer/config/input/config_command_report.py +267 -0
- model_analyzer/config/input/config_defaults.py +236 -0
- model_analyzer/config/input/config_enum.py +83 -0
- model_analyzer/config/input/config_field.py +216 -0
- model_analyzer/config/input/config_list_generic.py +112 -0
- model_analyzer/config/input/config_list_numeric.py +151 -0
- model_analyzer/config/input/config_list_string.py +111 -0
- model_analyzer/config/input/config_none.py +71 -0
- model_analyzer/config/input/config_object.py +129 -0
- model_analyzer/config/input/config_primitive.py +81 -0
- model_analyzer/config/input/config_status.py +75 -0
- model_analyzer/config/input/config_sweep.py +83 -0
- model_analyzer/config/input/config_union.py +113 -0
- model_analyzer/config/input/config_utils.py +128 -0
- model_analyzer/config/input/config_value.py +243 -0
- model_analyzer/config/input/objects/__init__.py +15 -0
- model_analyzer/config/input/objects/config_model_profile_spec.py +325 -0
- model_analyzer/config/input/objects/config_model_report_spec.py +173 -0
- model_analyzer/config/input/objects/config_plot.py +198 -0
- model_analyzer/config/input/objects/config_protobuf_utils.py +101 -0
- model_analyzer/config/input/yaml_config_validator.py +82 -0
- model_analyzer/config/run/__init__.py +15 -0
- model_analyzer/config/run/model_run_config.py +313 -0
- model_analyzer/config/run/run_config.py +168 -0
- model_analyzer/constants.py +76 -0
- model_analyzer/device/__init__.py +15 -0
- model_analyzer/device/device.py +24 -0
- model_analyzer/device/gpu_device.py +87 -0
- model_analyzer/device/gpu_device_factory.py +248 -0
- model_analyzer/entrypoint.py +307 -0
- model_analyzer/log_formatter.py +65 -0
- model_analyzer/model_analyzer_exceptions.py +24 -0
- model_analyzer/model_manager.py +255 -0
- model_analyzer/monitor/__init__.py +15 -0
- model_analyzer/monitor/cpu_monitor.py +69 -0
- model_analyzer/monitor/dcgm/DcgmDiag.py +191 -0
- model_analyzer/monitor/dcgm/DcgmFieldGroup.py +83 -0
- model_analyzer/monitor/dcgm/DcgmGroup.py +815 -0
- model_analyzer/monitor/dcgm/DcgmHandle.py +141 -0
- model_analyzer/monitor/dcgm/DcgmJsonReader.py +69 -0
- model_analyzer/monitor/dcgm/DcgmReader.py +623 -0
- model_analyzer/monitor/dcgm/DcgmStatus.py +57 -0
- model_analyzer/monitor/dcgm/DcgmSystem.py +412 -0
- model_analyzer/monitor/dcgm/__init__.py +15 -0
- model_analyzer/monitor/dcgm/common/__init__.py +13 -0
- model_analyzer/monitor/dcgm/common/dcgm_client_cli_parser.py +194 -0
- model_analyzer/monitor/dcgm/common/dcgm_client_main.py +86 -0
- model_analyzer/monitor/dcgm/dcgm_agent.py +887 -0
- model_analyzer/monitor/dcgm/dcgm_collectd_plugin.py +369 -0
- model_analyzer/monitor/dcgm/dcgm_errors.py +395 -0
- model_analyzer/monitor/dcgm/dcgm_field_helpers.py +546 -0
- model_analyzer/monitor/dcgm/dcgm_fields.py +815 -0
- model_analyzer/monitor/dcgm/dcgm_fields_collectd.py +671 -0
- model_analyzer/monitor/dcgm/dcgm_fields_internal.py +29 -0
- model_analyzer/monitor/dcgm/dcgm_fluentd.py +45 -0
- model_analyzer/monitor/dcgm/dcgm_monitor.py +138 -0
- model_analyzer/monitor/dcgm/dcgm_prometheus.py +326 -0
- model_analyzer/monitor/dcgm/dcgm_structs.py +2357 -0
- model_analyzer/monitor/dcgm/dcgm_telegraf.py +65 -0
- model_analyzer/monitor/dcgm/dcgm_value.py +151 -0
- model_analyzer/monitor/dcgm/dcgmvalue.py +155 -0
- model_analyzer/monitor/dcgm/denylist_recommendations.py +573 -0
- model_analyzer/monitor/dcgm/pydcgm.py +47 -0
- model_analyzer/monitor/monitor.py +143 -0
- model_analyzer/monitor/remote_monitor.py +137 -0
- model_analyzer/output/__init__.py +15 -0
- model_analyzer/output/file_writer.py +63 -0
- model_analyzer/output/output_writer.py +42 -0
- model_analyzer/perf_analyzer/__init__.py +15 -0
- model_analyzer/perf_analyzer/genai_perf_config.py +206 -0
- model_analyzer/perf_analyzer/perf_analyzer.py +882 -0
- model_analyzer/perf_analyzer/perf_config.py +479 -0
- model_analyzer/plots/__init__.py +15 -0
- model_analyzer/plots/detailed_plot.py +266 -0
- model_analyzer/plots/plot_manager.py +224 -0
- model_analyzer/plots/simple_plot.py +213 -0
- model_analyzer/record/__init__.py +15 -0
- model_analyzer/record/gpu_record.py +68 -0
- model_analyzer/record/metrics_manager.py +887 -0
- model_analyzer/record/record.py +280 -0
- model_analyzer/record/record_aggregator.py +256 -0
- model_analyzer/record/types/__init__.py +15 -0
- model_analyzer/record/types/cpu_available_ram.py +93 -0
- model_analyzer/record/types/cpu_used_ram.py +93 -0
- model_analyzer/record/types/gpu_free_memory.py +96 -0
- model_analyzer/record/types/gpu_power_usage.py +107 -0
- model_analyzer/record/types/gpu_total_memory.py +96 -0
- model_analyzer/record/types/gpu_used_memory.py +96 -0
- model_analyzer/record/types/gpu_utilization.py +108 -0
- model_analyzer/record/types/inter_token_latency_avg.py +60 -0
- model_analyzer/record/types/inter_token_latency_base.py +74 -0
- model_analyzer/record/types/inter_token_latency_max.py +60 -0
- model_analyzer/record/types/inter_token_latency_min.py +60 -0
- model_analyzer/record/types/inter_token_latency_p25.py +60 -0
- model_analyzer/record/types/inter_token_latency_p50.py +60 -0
- model_analyzer/record/types/inter_token_latency_p75.py +60 -0
- model_analyzer/record/types/inter_token_latency_p90.py +60 -0
- model_analyzer/record/types/inter_token_latency_p95.py +60 -0
- model_analyzer/record/types/inter_token_latency_p99.py +60 -0
- model_analyzer/record/types/output_token_throughput.py +105 -0
- model_analyzer/record/types/perf_client_response_wait.py +97 -0
- model_analyzer/record/types/perf_client_send_recv.py +97 -0
- model_analyzer/record/types/perf_latency.py +111 -0
- model_analyzer/record/types/perf_latency_avg.py +60 -0
- model_analyzer/record/types/perf_latency_base.py +74 -0
- model_analyzer/record/types/perf_latency_p90.py +60 -0
- model_analyzer/record/types/perf_latency_p95.py +60 -0
- model_analyzer/record/types/perf_latency_p99.py +60 -0
- model_analyzer/record/types/perf_server_compute_infer.py +97 -0
- model_analyzer/record/types/perf_server_compute_input.py +97 -0
- model_analyzer/record/types/perf_server_compute_output.py +97 -0
- model_analyzer/record/types/perf_server_queue.py +97 -0
- model_analyzer/record/types/perf_throughput.py +105 -0
- model_analyzer/record/types/time_to_first_token_avg.py +60 -0
- model_analyzer/record/types/time_to_first_token_base.py +74 -0
- model_analyzer/record/types/time_to_first_token_max.py +60 -0
- model_analyzer/record/types/time_to_first_token_min.py +60 -0
- model_analyzer/record/types/time_to_first_token_p25.py +60 -0
- model_analyzer/record/types/time_to_first_token_p50.py +60 -0
- model_analyzer/record/types/time_to_first_token_p75.py +60 -0
- model_analyzer/record/types/time_to_first_token_p90.py +60 -0
- model_analyzer/record/types/time_to_first_token_p95.py +60 -0
- model_analyzer/record/types/time_to_first_token_p99.py +60 -0
- model_analyzer/reports/__init__.py +15 -0
- model_analyzer/reports/html_report.py +195 -0
- model_analyzer/reports/pdf_report.py +50 -0
- model_analyzer/reports/report.py +86 -0
- model_analyzer/reports/report_factory.py +62 -0
- model_analyzer/reports/report_manager.py +1376 -0
- model_analyzer/reports/report_utils.py +42 -0
- model_analyzer/result/__init__.py +15 -0
- model_analyzer/result/constraint_manager.py +150 -0
- model_analyzer/result/model_config_measurement.py +354 -0
- model_analyzer/result/model_constraints.py +105 -0
- model_analyzer/result/parameter_search.py +246 -0
- model_analyzer/result/result_manager.py +430 -0
- model_analyzer/result/result_statistics.py +159 -0
- model_analyzer/result/result_table.py +217 -0
- model_analyzer/result/result_table_manager.py +646 -0
- model_analyzer/result/result_utils.py +42 -0
- model_analyzer/result/results.py +277 -0
- model_analyzer/result/run_config_measurement.py +658 -0
- model_analyzer/result/run_config_result.py +210 -0
- model_analyzer/result/run_config_result_comparator.py +110 -0
- model_analyzer/result/sorted_results.py +151 -0
- model_analyzer/state/__init__.py +15 -0
- model_analyzer/state/analyzer_state.py +76 -0
- model_analyzer/state/analyzer_state_manager.py +215 -0
- model_analyzer/triton/__init__.py +15 -0
- model_analyzer/triton/client/__init__.py +15 -0
- model_analyzer/triton/client/client.py +234 -0
- model_analyzer/triton/client/client_factory.py +57 -0
- model_analyzer/triton/client/grpc_client.py +104 -0
- model_analyzer/triton/client/http_client.py +107 -0
- model_analyzer/triton/model/__init__.py +15 -0
- model_analyzer/triton/model/model_config.py +556 -0
- model_analyzer/triton/model/model_config_variant.py +29 -0
- model_analyzer/triton/server/__init__.py +15 -0
- model_analyzer/triton/server/server.py +76 -0
- model_analyzer/triton/server/server_config.py +269 -0
- model_analyzer/triton/server/server_docker.py +229 -0
- model_analyzer/triton/server/server_factory.py +306 -0
- model_analyzer/triton/server/server_local.py +158 -0
- triton_model_analyzer-1.48.0.dist-info/METADATA +52 -0
- triton_model_analyzer-1.48.0.dist-info/RECORD +204 -0
- triton_model_analyzer-1.48.0.dist-info/WHEEL +5 -0
- triton_model_analyzer-1.48.0.dist-info/entry_points.txt +2 -0
- triton_model_analyzer-1.48.0.dist-info/licenses/LICENSE +67 -0
- triton_model_analyzer-1.48.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
from matplotlib import patches as mpatches
|
|
23
|
+
|
|
24
|
+
from model_analyzer.constants import LOGGER_NAME
|
|
25
|
+
from model_analyzer.record.metrics_manager import MetricsManager
|
|
26
|
+
|
|
27
|
+
logging.getLogger("matplotlib").setLevel(logging.ERROR)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(LOGGER_NAME)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DetailedPlot:
|
|
33
|
+
"""
|
|
34
|
+
A wrapper class around a matplotlib
|
|
35
|
+
plot that adapts with the kinds of
|
|
36
|
+
plots the model analyzer wants to generates
|
|
37
|
+
|
|
38
|
+
Detailed plots detail th
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
detailed_metrics = [
|
|
42
|
+
"perf_server_queue",
|
|
43
|
+
"perf_server_compute_input",
|
|
44
|
+
"perf_server_compute_infer",
|
|
45
|
+
"perf_server_compute_output",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def __init__(self, name, title, bar_width=0.5):
|
|
49
|
+
"""
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
name: str
|
|
53
|
+
The name of the file that the plot
|
|
54
|
+
will be saved as
|
|
55
|
+
title : str
|
|
56
|
+
The title of this plot/figure
|
|
57
|
+
bar_width: float
|
|
58
|
+
width of the latency breakdown bars
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
self._name = name
|
|
62
|
+
self._title = title
|
|
63
|
+
|
|
64
|
+
self._fig, self._ax_latency = plt.subplots()
|
|
65
|
+
self._ax_latency.set_title(title)
|
|
66
|
+
self._ax_throughput = self._ax_latency.twinx()
|
|
67
|
+
|
|
68
|
+
latency_axis_label, throughput_axis_label = [
|
|
69
|
+
metric.header(aggregation_tag="")
|
|
70
|
+
for metric in MetricsManager.get_metric_types(
|
|
71
|
+
["perf_latency_avg", "perf_throughput"]
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# Okabe-Ito guideline colors for colorblind
|
|
76
|
+
# https://jfly.uni-koeln.de/color/#select
|
|
77
|
+
self._bar_colors = {
|
|
78
|
+
"perf_server_queue": "#e69f00",
|
|
79
|
+
"perf_server_compute_input": "#56b4e9",
|
|
80
|
+
"perf_server_compute_infer": "#009e73",
|
|
81
|
+
"perf_server_compute_output": "#f0e442",
|
|
82
|
+
"perf_throughput": "#000000",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
self._bar_width = bar_width
|
|
86
|
+
self._legend_x = 0.92
|
|
87
|
+
self._legend_y = 1.15
|
|
88
|
+
self._legend_font_size = 10
|
|
89
|
+
self._fig.set_figheight(8)
|
|
90
|
+
self._fig.set_figwidth(12)
|
|
91
|
+
|
|
92
|
+
self._ax_latency.set_xlabel("Concurrent Client Requests")
|
|
93
|
+
self._ax_latency.set_ylabel(latency_axis_label)
|
|
94
|
+
self._ax_throughput.set_ylabel(throughput_axis_label)
|
|
95
|
+
|
|
96
|
+
self._data = defaultdict(list)
|
|
97
|
+
|
|
98
|
+
def data(self):
|
|
99
|
+
"""
|
|
100
|
+
Get the data in this plot
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
dict
|
|
105
|
+
keys are line labels
|
|
106
|
+
and values are lists of floats
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
return self._data
|
|
110
|
+
|
|
111
|
+
def add_run_config_measurement(self, run_config_measurement):
|
|
112
|
+
"""
|
|
113
|
+
Adds a measurement to this plot
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
measurement : Measurement
|
|
118
|
+
The measurement containing the data to
|
|
119
|
+
be plotted.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# TODO-TMA-568: This needs to be updated because there will be multiple model configs
|
|
123
|
+
if (
|
|
124
|
+
"concurrency-range" in run_config_measurement.model_specific_pa_params()[0]
|
|
125
|
+
and run_config_measurement.model_specific_pa_params()[0][
|
|
126
|
+
"concurrency-range"
|
|
127
|
+
]
|
|
128
|
+
):
|
|
129
|
+
self._data["concurrency"].append(
|
|
130
|
+
run_config_measurement.model_specific_pa_params()[0][
|
|
131
|
+
"concurrency-range"
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
"request-rate-range" in run_config_measurement.model_specific_pa_params()[0]
|
|
137
|
+
and run_config_measurement.model_specific_pa_params()[0][
|
|
138
|
+
"request-rate-range"
|
|
139
|
+
]
|
|
140
|
+
):
|
|
141
|
+
self._data["request_rate"].append(
|
|
142
|
+
run_config_measurement.model_specific_pa_params()[0][
|
|
143
|
+
"request-rate-range"
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self._data["perf_throughput"].append(
|
|
148
|
+
run_config_measurement.get_non_gpu_metric_value(tag="perf_throughput")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for metric in self.detailed_metrics:
|
|
152
|
+
if MetricsManager.is_gpu_metric(tag=metric):
|
|
153
|
+
self._data[metric].append(
|
|
154
|
+
run_config_measurement.get_gpu_metric_value(tag=metric)
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
self._data[metric].append(
|
|
158
|
+
run_config_measurement.get_non_gpu_metric_value(tag=metric)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def plot_data(self):
|
|
162
|
+
"""
|
|
163
|
+
Calls plotting function
|
|
164
|
+
on this plot's Axes object
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# Need to change the default x-axis plot title for request rates
|
|
168
|
+
if "request_rate" in self._data and self._data["request_rate"][0]:
|
|
169
|
+
self._ax_latency.set_xlabel("Client Request Rate")
|
|
170
|
+
|
|
171
|
+
# Sort the data by request rate or concurrency
|
|
172
|
+
if "request_rate" in self._data and self._data["request_rate"][0]:
|
|
173
|
+
sort_indices = list(
|
|
174
|
+
zip(*sorted(enumerate(self._data["request_rate"]), key=lambda x: x[1]))
|
|
175
|
+
)[0]
|
|
176
|
+
else:
|
|
177
|
+
sort_indices = list(
|
|
178
|
+
zip(*sorted(enumerate(self._data["concurrency"]), key=lambda x: x[1]))
|
|
179
|
+
)[0]
|
|
180
|
+
|
|
181
|
+
sorted_data = {
|
|
182
|
+
key: [data_list[i] for i in sort_indices]
|
|
183
|
+
for key, data_list in self._data.items()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Plot latency breakdown bars
|
|
187
|
+
labels = dict(
|
|
188
|
+
zip(
|
|
189
|
+
self.detailed_metrics,
|
|
190
|
+
[
|
|
191
|
+
metric.header()
|
|
192
|
+
for metric in MetricsManager.get_metric_types(
|
|
193
|
+
tags=self.detailed_metrics
|
|
194
|
+
)
|
|
195
|
+
],
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
bottoms = None
|
|
199
|
+
|
|
200
|
+
if "request_rate" in self._data:
|
|
201
|
+
sorted_data["indices"] = list(map(str, sorted_data["request_rate"]))
|
|
202
|
+
else:
|
|
203
|
+
sorted_data["indices"] = list(map(str, sorted_data["concurrency"]))
|
|
204
|
+
|
|
205
|
+
# Plot latency breakdown with concurrency casted as string to make uniform x
|
|
206
|
+
for metric, label in labels.items():
|
|
207
|
+
self._ax_latency.bar(
|
|
208
|
+
sorted_data["indices"],
|
|
209
|
+
sorted_data[metric],
|
|
210
|
+
width=self._bar_width,
|
|
211
|
+
label=label,
|
|
212
|
+
bottom=bottoms,
|
|
213
|
+
color=self._bar_colors[metric],
|
|
214
|
+
)
|
|
215
|
+
if not bottoms:
|
|
216
|
+
bottoms = sorted_data[metric]
|
|
217
|
+
else:
|
|
218
|
+
bottoms = list(map(lambda x, y: x + y, bottoms, sorted_data[metric]))
|
|
219
|
+
|
|
220
|
+
# Plot the inference line
|
|
221
|
+
inference_line = self._ax_throughput.plot(
|
|
222
|
+
sorted_data["indices"],
|
|
223
|
+
sorted_data["perf_throughput"],
|
|
224
|
+
label="Inferences/second",
|
|
225
|
+
marker="o",
|
|
226
|
+
color=self._bar_colors["perf_throughput"],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Create legend handles
|
|
230
|
+
handles = [
|
|
231
|
+
mpatches.Patch(color=self._bar_colors[m], label=labels[m])
|
|
232
|
+
for m in self._bar_colors
|
|
233
|
+
if m != "perf_throughput"
|
|
234
|
+
]
|
|
235
|
+
handles.append(inference_line[0])
|
|
236
|
+
|
|
237
|
+
self._ax_latency.legend(
|
|
238
|
+
handles=handles,
|
|
239
|
+
ncol=(len(self._bar_colors) // 2) + 1,
|
|
240
|
+
bbox_to_anchor=(self._legend_x, self._legend_y),
|
|
241
|
+
prop=dict(size=self._legend_font_size),
|
|
242
|
+
)
|
|
243
|
+
# Annotate inferences
|
|
244
|
+
for x, y in zip(sorted_data["indices"], sorted_data["perf_throughput"]):
|
|
245
|
+
self._ax_throughput.annotate(
|
|
246
|
+
str(round(y, 2)),
|
|
247
|
+
xy=(x, y),
|
|
248
|
+
textcoords="offset points", # how to position the text
|
|
249
|
+
xytext=(0, 10), # distance from text to points (x,y)
|
|
250
|
+
ha="center",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self._ax_latency.grid()
|
|
254
|
+
self._ax_latency.set_axisbelow(True)
|
|
255
|
+
|
|
256
|
+
def save(self, filepath):
|
|
257
|
+
"""
|
|
258
|
+
Saves a .png of the plot to disk
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
filepath : the path to the directory
|
|
263
|
+
this plot should be saved to
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
self._fig.savefig(os.path.join(filepath, self._name))
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from typing import DefaultDict, Dict, Union
|
|
20
|
+
|
|
21
|
+
from model_analyzer.config.input.config_command_profile import ConfigCommandProfile
|
|
22
|
+
from model_analyzer.config.input.config_command_report import ConfigCommandReport
|
|
23
|
+
from model_analyzer.config.input.config_defaults import DEFAULT_CPU_MEM_PLOT
|
|
24
|
+
from model_analyzer.config.input.objects.config_plot import ConfigPlot
|
|
25
|
+
from model_analyzer.constants import GLOBAL_CONSTRAINTS_KEY, TOP_MODELS_REPORT_KEY
|
|
26
|
+
from model_analyzer.result.constraint_manager import ConstraintManager
|
|
27
|
+
from model_analyzer.result.result_manager import ResultManager
|
|
28
|
+
|
|
29
|
+
from .detailed_plot import DetailedPlot
|
|
30
|
+
from .simple_plot import SimplePlot
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PlotManager:
|
|
34
|
+
"""
|
|
35
|
+
This class manages the construction and arrangement
|
|
36
|
+
of plots generated by model analyzer
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
config: Union[ConfigCommandProfile, ConfigCommandReport],
|
|
42
|
+
result_manager: ResultManager,
|
|
43
|
+
constraint_manager: ConstraintManager,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
config : ConfigCommandProfile or ConfigCommandReport
|
|
49
|
+
The model analyzer's config containing information
|
|
50
|
+
about the kind of plots to generate
|
|
51
|
+
result_manager : ResultManager
|
|
52
|
+
instance that manages the result tables and
|
|
53
|
+
adding results
|
|
54
|
+
constraint_manager: ConstraintManager
|
|
55
|
+
instance that manages constraints
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
self._config = config
|
|
59
|
+
self._result_manager = result_manager
|
|
60
|
+
|
|
61
|
+
# Constraints should be plotted as well
|
|
62
|
+
self._constraints = constraint_manager.get_constraints_for_all_models()
|
|
63
|
+
|
|
64
|
+
# Construct plot output directory
|
|
65
|
+
self._plot_export_directory = os.path.join(config.export_path, "plots")
|
|
66
|
+
os.makedirs(self._plot_export_directory, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
# Dict of list of plots
|
|
69
|
+
self._simple_plots: DefaultDict[str, Dict[str, SimplePlot]] = defaultdict()
|
|
70
|
+
self._detailed_plots: Dict[str, DetailedPlot] = {}
|
|
71
|
+
|
|
72
|
+
def create_summary_plots(self):
|
|
73
|
+
"""
|
|
74
|
+
Constructs simple plots based on config specs
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
model_names = self._result_manager._profile_model_names
|
|
78
|
+
|
|
79
|
+
for plots_key in model_names:
|
|
80
|
+
self._create_summary_plot_for_model(
|
|
81
|
+
plots_key=plots_key,
|
|
82
|
+
model_name=plots_key,
|
|
83
|
+
num_results=self._config.num_configs_per_model,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if self._config.num_top_model_configs:
|
|
87
|
+
self._create_summary_plot_for_model(
|
|
88
|
+
plots_key=TOP_MODELS_REPORT_KEY,
|
|
89
|
+
model_name=None,
|
|
90
|
+
num_results=self._config.num_top_model_configs,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _create_summary_plot_for_model(self, model_name, plots_key, num_results):
|
|
94
|
+
"""
|
|
95
|
+
helper function that creates the summary plots
|
|
96
|
+
for a given model
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
for plot_config in self._config.plots:
|
|
100
|
+
constraints = self._constraints[GLOBAL_CONSTRAINTS_KEY]
|
|
101
|
+
if plots_key in self._constraints:
|
|
102
|
+
constraints = self._constraints[plots_key]
|
|
103
|
+
for run_config_result in self._result_manager.top_n_results(
|
|
104
|
+
model_name=model_name, n=num_results, include_default=True
|
|
105
|
+
):
|
|
106
|
+
if run_config_result.run_config().cpu_only():
|
|
107
|
+
if plot_config.y_axis() == "gpu_used_memory":
|
|
108
|
+
plot_name, plot_config_dict = list(
|
|
109
|
+
DEFAULT_CPU_MEM_PLOT.items()
|
|
110
|
+
)[0]
|
|
111
|
+
plot_config = ConfigPlot(plot_name, **plot_config_dict)
|
|
112
|
+
self._create_update_simple_plot(
|
|
113
|
+
plots_key=plots_key,
|
|
114
|
+
plot_config=plot_config,
|
|
115
|
+
run_config_measurements=run_config_result.run_config_measurements(),
|
|
116
|
+
constraints=constraints,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _create_update_simple_plot(
|
|
120
|
+
self, plots_key, plot_config, run_config_measurements, constraints
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Creates or updates a single simple plot, given a config name,
|
|
124
|
+
some measurements, and a key to put the plot into the simple plots
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if plots_key not in self._simple_plots:
|
|
128
|
+
self._simple_plots[plots_key] = {}
|
|
129
|
+
if plot_config.name() not in self._simple_plots[plots_key]:
|
|
130
|
+
self._simple_plots[plots_key][plot_config.name()] = SimplePlot(
|
|
131
|
+
name=plot_config.name(),
|
|
132
|
+
title=plot_config.title(),
|
|
133
|
+
x_axis=plot_config.x_axis(),
|
|
134
|
+
y_axis=plot_config.y_axis(),
|
|
135
|
+
monotonic=plot_config.monotonic(),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for run_config_measurement in run_config_measurements:
|
|
139
|
+
self._simple_plots[plots_key][
|
|
140
|
+
plot_config.name()
|
|
141
|
+
].add_run_config_measurement(
|
|
142
|
+
label=run_config_measurement.model_variants_name(),
|
|
143
|
+
run_config_measurement=run_config_measurement,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# In case this plot already had lines, we want to clear and replot
|
|
147
|
+
self._simple_plots[plots_key][plot_config.name()].clear()
|
|
148
|
+
self._simple_plots[plots_key][plot_config.name()].plot_data_and_constraints(
|
|
149
|
+
constraints=constraints
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def create_detailed_plots(self):
|
|
153
|
+
"""
|
|
154
|
+
Constructs detailed plots based on
|
|
155
|
+
requested config specs
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
# Create detailed plots
|
|
159
|
+
for model in self._config.report_model_configs:
|
|
160
|
+
model_config_name = model.model_config_name()
|
|
161
|
+
self._detailed_plots[model_config_name] = DetailedPlot(
|
|
162
|
+
f"latency_breakdown", "Online Performance"
|
|
163
|
+
)
|
|
164
|
+
(
|
|
165
|
+
model_config,
|
|
166
|
+
run_config_measurements,
|
|
167
|
+
) = self._result_manager.get_model_configs_run_config_measurements(
|
|
168
|
+
model_config_name
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# If model_config_name was present in results
|
|
172
|
+
if run_config_measurements:
|
|
173
|
+
for run_config_measurement in run_config_measurements:
|
|
174
|
+
self._detailed_plots[model_config_name].add_run_config_measurement(
|
|
175
|
+
run_config_measurement
|
|
176
|
+
)
|
|
177
|
+
self._detailed_plots[model_config_name].plot_data()
|
|
178
|
+
|
|
179
|
+
# Create the simple plots for the detailed reports
|
|
180
|
+
for plot_config in model.plots():
|
|
181
|
+
if model_config.cpu_only() and (
|
|
182
|
+
plot_config.y_axis().startswith("gpu_")
|
|
183
|
+
or plot_config.x_axis().startswith("gpu_")
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
self._create_update_simple_plot(
|
|
187
|
+
plots_key=model_config_name,
|
|
188
|
+
plot_config=plot_config,
|
|
189
|
+
run_config_measurements=run_config_measurements,
|
|
190
|
+
constraints=None,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def export_summary_plots(self):
|
|
194
|
+
"""
|
|
195
|
+
write the plots to disk
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
simple_plot_dir = os.path.join(self._plot_export_directory, "simple")
|
|
199
|
+
for plots_key, plot_dicts in self._simple_plots.items():
|
|
200
|
+
model_plot_dir = os.path.join(simple_plot_dir, plots_key)
|
|
201
|
+
os.makedirs(model_plot_dir, exist_ok=True)
|
|
202
|
+
for plot in plot_dicts.values():
|
|
203
|
+
plot.save(model_plot_dir)
|
|
204
|
+
|
|
205
|
+
def export_detailed_plots(self):
|
|
206
|
+
"""
|
|
207
|
+
Write detailed plots to disk
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
detailed_plot_dir = os.path.join(self._plot_export_directory, "detailed")
|
|
211
|
+
simple_plot_dir = os.path.join(self._plot_export_directory, "simple")
|
|
212
|
+
for model_config_name, detailed_plot in self._detailed_plots.items():
|
|
213
|
+
detailed_model_config_plot_dir = os.path.join(
|
|
214
|
+
detailed_plot_dir, model_config_name
|
|
215
|
+
)
|
|
216
|
+
os.makedirs(detailed_model_config_plot_dir, exist_ok=True)
|
|
217
|
+
detailed_plot.save(detailed_model_config_plot_dir)
|
|
218
|
+
|
|
219
|
+
simple_model_config_plot_dir = os.path.join(
|
|
220
|
+
simple_plot_dir, model_config_name
|
|
221
|
+
)
|
|
222
|
+
os.makedirs(simple_model_config_plot_dir, exist_ok=True)
|
|
223
|
+
for simple_plot in self._simple_plots[model_config_name].values():
|
|
224
|
+
simple_plot.save(simple_model_config_plot_dir)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright 2021-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
|
|
22
|
+
from model_analyzer.perf_analyzer.perf_config import PerfAnalyzerConfig
|
|
23
|
+
from model_analyzer.record.metrics_manager import MetricsManager
|
|
24
|
+
from model_analyzer.reports.report_utils import truncate_model_config_name
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimplePlot:
|
|
28
|
+
"""
|
|
29
|
+
A wrapper class around a matplotlib
|
|
30
|
+
plot that adapts with the kinds of
|
|
31
|
+
plots the model analyzer wants to generates
|
|
32
|
+
|
|
33
|
+
A singe plot holds data for multiple
|
|
34
|
+
model configs, but only holds one
|
|
35
|
+
type of plot
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, name, title, x_axis, y_axis, monotonic=False):
|
|
39
|
+
"""
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
name: str
|
|
43
|
+
The name of the file that the plot
|
|
44
|
+
will be saved as
|
|
45
|
+
title : str
|
|
46
|
+
The title of this plot/figure
|
|
47
|
+
x_axis : str
|
|
48
|
+
The metric tag for the x-axis of this plot
|
|
49
|
+
y_axis : str
|
|
50
|
+
The metric tag for the y-axis of this plot
|
|
51
|
+
monotonic: bool
|
|
52
|
+
Whether or not to prune decreasing points in this
|
|
53
|
+
plot
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
self._name = name
|
|
57
|
+
self._title = title
|
|
58
|
+
self._x_axis = x_axis
|
|
59
|
+
self._y_axis = y_axis
|
|
60
|
+
self._monotonic = monotonic
|
|
61
|
+
|
|
62
|
+
self._fig, self._ax = plt.subplots()
|
|
63
|
+
|
|
64
|
+
self._data = {}
|
|
65
|
+
|
|
66
|
+
def add_run_config_measurement(self, label, run_config_measurement):
|
|
67
|
+
"""
|
|
68
|
+
Adds a measurement to this plot
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
label : str
|
|
73
|
+
The name of the config(s) this measurement
|
|
74
|
+
is taken from.
|
|
75
|
+
run_config_measurement : RunConfigMeasurement
|
|
76
|
+
The measurement containing the data to
|
|
77
|
+
be plotted.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
if label not in self._data:
|
|
81
|
+
self._data[label] = defaultdict(list)
|
|
82
|
+
|
|
83
|
+
if self._x_axis.replace("_", "-") in PerfAnalyzerConfig.allowed_keys():
|
|
84
|
+
self._data[label]["x_data"].append(
|
|
85
|
+
run_config_measurement.model_specific_pa_params()[0][
|
|
86
|
+
self._x_axis.replace("_", "-")
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
if MetricsManager.is_gpu_metric(tag=self._x_axis):
|
|
91
|
+
self._data[label]["x_data"].append(
|
|
92
|
+
run_config_measurement.get_gpu_metric_value(tag=self._x_axis)
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
self._data[label]["x_data"].append(
|
|
96
|
+
run_config_measurement.get_non_gpu_metric_value(tag=self._x_axis)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if self._y_axis.replace("_", "-") in PerfAnalyzerConfig.allowed_keys():
|
|
100
|
+
self._data[label]["y_data"].append(
|
|
101
|
+
run_config_measurement.model_specific_pa_params()[0][
|
|
102
|
+
self._y_axis.replace("_", "-")
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
if MetricsManager.is_gpu_metric(tag=self._y_axis):
|
|
107
|
+
self._data[label]["y_data"].append(
|
|
108
|
+
run_config_measurement.get_gpu_metric_value(tag=self._y_axis)
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
self._data[label]["y_data"].append(
|
|
112
|
+
run_config_measurement.get_non_gpu_metric_value(tag=self._y_axis)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def clear(self):
|
|
116
|
+
"""
|
|
117
|
+
Clear the contents of the current Axes object
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
self._ax.clear()
|
|
121
|
+
|
|
122
|
+
def plot_data_and_constraints(self, constraints):
|
|
123
|
+
"""
|
|
124
|
+
Calls plotting function
|
|
125
|
+
on this plot's Axes object
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
constraints: ModelConstraints object
|
|
130
|
+
The keys are metric tags and values are dicts whose
|
|
131
|
+
keys are constraint types (min, max) and values are their
|
|
132
|
+
values
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
self._ax.set_title(self._title)
|
|
136
|
+
|
|
137
|
+
if self._x_axis.replace("_", "-") in PerfAnalyzerConfig.allowed_keys():
|
|
138
|
+
self._x_header = self._x_axis.replace("_", " ").title()
|
|
139
|
+
else:
|
|
140
|
+
self._x_header = MetricsManager.get_metric_types([self._x_axis])[0].header(
|
|
141
|
+
aggregation_tag=""
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if self._y_axis.replace("_", "-") in PerfAnalyzerConfig.allowed_keys():
|
|
145
|
+
self._y_header = self._y_axis.replace("_", " ").title()
|
|
146
|
+
else:
|
|
147
|
+
self._y_header = MetricsManager.get_metric_types([self._y_axis])[0].header(
|
|
148
|
+
aggregation_tag=""
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self._ax.set_xlabel(self._x_header)
|
|
152
|
+
self._ax.set_ylabel(self._y_header)
|
|
153
|
+
|
|
154
|
+
for model_config_name, data in self._data.items():
|
|
155
|
+
# Sort the data by x-axis
|
|
156
|
+
x_data, y_data = (
|
|
157
|
+
list(t) for t in zip(*sorted(zip(data["x_data"], data["y_data"])))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
model_config_name = truncate_model_config_name(model_config_name)
|
|
161
|
+
|
|
162
|
+
if self._monotonic:
|
|
163
|
+
filtered_x, filtered_y = [x_data[0]], [y_data[0]]
|
|
164
|
+
for i in range(1, len(x_data)):
|
|
165
|
+
if y_data[i] > filtered_y[-1]:
|
|
166
|
+
filtered_x.append(x_data[i])
|
|
167
|
+
filtered_y.append(y_data[i])
|
|
168
|
+
x_data, y_data = filtered_x, filtered_y
|
|
169
|
+
|
|
170
|
+
self._ax.plot(x_data, y_data, marker="o", label=model_config_name)
|
|
171
|
+
|
|
172
|
+
# Plot constraints
|
|
173
|
+
if constraints:
|
|
174
|
+
if constraints.has_metric(self._x_axis):
|
|
175
|
+
for _, constraint_val in constraints[self._x_axis].items():
|
|
176
|
+
constraint_label = f"Target {self._x_header.rsplit(' ',1)[0]}"
|
|
177
|
+
self._ax.axvline(
|
|
178
|
+
x=constraint_val, linestyle="--", label=constraint_label
|
|
179
|
+
)
|
|
180
|
+
if constraints.has_metric(self._y_axis):
|
|
181
|
+
for _, constraint_val in constraints[self._y_axis].items():
|
|
182
|
+
constraint_label = f"Target {self._y_header.rsplit(' ', 1)[0]}"
|
|
183
|
+
self._ax.axhline(
|
|
184
|
+
y=constraint_val, linestyle="--", label=constraint_label
|
|
185
|
+
)
|
|
186
|
+
# plot h lines
|
|
187
|
+
self._ax.legend()
|
|
188
|
+
self._ax.grid()
|
|
189
|
+
|
|
190
|
+
def data(self):
|
|
191
|
+
"""
|
|
192
|
+
Get the data in this plot
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
dict
|
|
197
|
+
keys are line labels
|
|
198
|
+
and values are lists of floats
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
return self._data
|
|
202
|
+
|
|
203
|
+
def save(self, filepath):
|
|
204
|
+
"""
|
|
205
|
+
Saves a .png of the plot to disk
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
filepath : the path to the directory
|
|
210
|
+
this plot should be saved to
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
self._fig.savefig(os.path.join(filepath, self._name))
|