From 66258bdf1b182bc2e5b6e08fe60ec34d4d7e982a Mon Sep 17 00:00:00 2001 From: Fengrui-Liu Date: Sun, 6 Nov 2022 19:35:37 +0800 Subject: [PATCH 1/3] Func: ray-based deployment --- README.md | 2 +- docs/static/metric-detector.png | Bin 0 -> 40118 bytes .../models/metric/detectors/spot_detector.py | 78 ++++++++++-------- engine/models/metric/serve/deployment.py | 58 +++++++++++++ engine/models/tests/test_detector.py | 41 +++++++-- 5 files changed, 138 insertions(+), 41 deletions(-) create mode 100644 docs/static/metric-detector.png create mode 100644 engine/models/metric/serve/deployment.py diff --git a/README.md b/README.md index 4f46032..37d5225 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ OSPP 2022 and GSOC 2022 student research outcomes. **Metric Anomaly Detection and Visualizations** -TBD - Soon to be added +![img.png](docs/static/metric-detector.png) ### Roadmap diff --git a/docs/static/metric-detector.png b/docs/static/metric-detector.png new file mode 100644 index 0000000000000000000000000000000000000000..f987b3bcef2cf677ba7b1957f7ca14b0f1af129c GIT binary patch literal 40118 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%0A5H)fDB_gb zz5&!Ck!RlwCD&g7-zX8Sq5T0afF{Ys-0M4BDSD`1VEi3W+i^qZ3Eld8@!iNwwx)Rs zhz-q0&{*nmkL-_Q)Zt_0){(>+8YtUz`2AvHGx{~&R^*V2tx&2jJoXCmAeRt*?gwty zMiK?v>DFS-lrwla7cwOLF41mjv%UlD!f$q3FrkZI&S#~kgNH45wVm88xY@1`jfLPs z;vRVGPAhtd>)zDVH1(GL7S5USO;wilTItdna1k0T?_xY;VaN8NGf^f;#ARl4sOE7G z8td>jAvYk$dF*sjSTbgFrGxW1nFdQ3@Y>2!dlBn0^uswI#4+{kK>_3gs_ZYW6h=rM z4b#SNAS@!`5-_n&bh`aMIT{BXb6$D+){(63d2igwaT~_fA=u2Y#3dH%hQsDlt3N3yMo^HFdkVu($ zZ3bwpbU@KrKvLT`U@=v5ux%Jx5D$6lGDU3fPxyj6^&$4yH|_ebrB=~kYnOMg?mLTJ z@8|Lhw?c83Rq69z-&~0m8ZBoBUNzfbO7Fi+K3;@-`T8ZMCtf_{J_h$q+PfN+HkSrnoUy5|i0AzXwrrlOcN}mpDg(u8T1CBBO5%k6z)? za~a5=SC~J&@4GUp+^WF);9|h86QsR&vEOcM}vF;Hx7ygoGuUkKf$OQHeo z$w)alntZ`MEgoHU`ME*dCRVgv0CCoDz-xZI`KGJvR@+{K5Um;_N2Wmw^fKA=iL!`; zGC2xep~a*a(vNI4cHy+ydiNzO7Egd4VGPmm3aC)X$~-@}s}%iczPUUYihHV|M&iHp z%&5~E%c7@XHbhYv94!O}whl(>Rz%yw5^ij+DA_;5sg(D<|gG3q_gIIo@Xo9m@{0jR+H^PzJ4DOD8s z__vJj4rYly*y801Xosh-;#mT}M`lk#5$X4^r6h{yA@2TBns6w?PV^3q3S~a3l?4bJH}!F|Cdv~)bjHct(y2|okQh8`YKcZ#pNOpz!-N!N zwaLUz`%tl@!S#1kV35|T~j}?B_@ARZhkZC_G*Wvp%uTtgFpxLednCBO0%F-4~Wl97gcgUWgJUB;t%cuIn($-f^#$n<$gT|s_xNqzLNfdCDDd_LOcv5Pe$f<$#A;J!IS zt3|dG^*?ExIV%SG7ufN%6P6*$$IC;cuI|H&9axV*V~V1t0aknEzS{nO5K2uuw*b*# z<@85odVR8jyZe6lm2^IRWXwrs?u{00@6rwGE*5Q5u4i<0(@!tU2q)r)soib*mENax z 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 literal 0 HcmV?d00001 diff --git a/engine/models/metric/detectors/spot_detector.py b/engine/models/metric/detectors/spot_detector.py index eb41d99..8e2449e 100644 --- a/engine/models/metric/detectors/spot_detector.py +++ b/engine/models/metric/detectors/spot_detector.py @@ -22,7 +22,7 @@ from ..base import BaseDetector -np.seterr(divide='ignore', invalid='ignore') +np.seterr(divide="ignore", invalid="ignore") class SpotDetector(BaseDetector): @@ -53,19 +53,19 @@ def __init__( self.back_mean_window = deque(maxlen=self.back_mean_len) assert ( self.window_len > 100 - ), 'window_len is too small, default value is 200' + ), "window_len is too small, default value is 200" self.num_threshold = { - 'up': num_threshold_up, - 'down': num_threshold_down, + "up": num_threshold_up, + "down": num_threshold_down, } - nonedict = {'up': None, 'down': None} + nonedict = {"up": None, "down": None} self.extreme_quantile = dict.copy(nonedict) self.init_threshold = dict.copy(nonedict) self.peaks = dict.copy(nonedict) - self.history_peaks = {'up': [], 'down': []} + self.history_peaks = {"up": [], "down": []} # self.peaks = {'up':deque(maxlen=20),'down':deque(maxlen=20)} self.gamma = dict.copy(nonedict) self.sigma = dict.copy(nonedict) @@ -96,7 +96,7 @@ def jac_w(y, t): ) * (1 - vs) jac_vs = np.divide( 1, t, out=np.array(1 / epsilon), where=t != 0 - ) * (-vs + np.mean(1 / s**2)) + ) * (-vs + np.mean(1 / s ** 2)) return us * jac_vs + vs * jac_us self.peaks[side][self.peaks[side] == 0] = epsilon @@ -117,7 +117,7 @@ def jac_w(y, t): ) c = 2 * np.divide( y_mean - y_min, - y_min**2, + y_min ** 2, out=np.array((y_mean - y_min) / epsilon + epsilon), where=y_min != 0, ) @@ -130,7 +130,7 @@ def jac_w(y, t): lambda t: jac_w(self.peaks[side], t), (d, e) if d < e else (e, d), n_points, - 'regular', + "regular", ) right_zeros = self._roots_finder( @@ -138,7 +138,7 @@ def jac_w(y, t): lambda t: jac_w(self.peaks[side], t), (b, c) if b < c else (c, b), n_points, - 'regular', + "regular", ) # all the possible roots @@ -183,13 +183,13 @@ def _roots_finder(self, fun, jac, bounds, npoints, method): numpy.array possible roots of the function """ - if method == 'regular': + if method == "regular": step = (bounds[1] - bounds[0]) / (npoints + 1) try: x_sampled = np.arange(bounds[0] + step, bounds[1], step) except BaseException: x_sampled = np.random.uniform(bounds[0], bounds[1], npoints) - elif method == 'random': + elif method == "random": x_sampled = np.random.uniform(bounds[0], bounds[1], npoints) def obj_fun(x_sampled, f, jac): @@ -198,7 +198,7 @@ def obj_fun(x_sampled, f, jac): i = 0 for x in x_sampled: fx = f(x) - g = g + fx**2 + g = g + fx ** 2 j[i] = 2 * fx * jac(x) i = i + 1 return g, j @@ -206,7 +206,7 @@ def obj_fun(x_sampled, f, jac): opt = minimize( lambda X: obj_fun(X, fun, jac), x_sampled, - method='L-BFGS-B', + method="L-BFGS-B", jac=True, bounds=[bounds] * len(x_sampled), ) @@ -245,7 +245,7 @@ def _log_likelihood(self, y, gamma, sigma): def _quantile(self, side, gamma, sigma): - if side == 'up': + if side == "up": r = self.window_len * self.prob / self.num_threshold[side] if gamma != 0: @@ -254,7 +254,7 @@ def _quantile(self, side, gamma, sigma): ) else: return self.init_threshold[side] - sigma * log(r) - elif side == 'down': + elif side == "down": r = self.window_len * self.prob / self.num_threshold[side] if gamma != 0: @@ -264,18 +264,18 @@ def _quantile(self, side, gamma, sigma): else: return self.init_threshold[side] + sigma * log(r) else: - raise ValueError('The side is not right') + raise ValueError("The side is not right") def _init_drift(self, verbose=False): - for side in ['up', 'down']: + for side in ["up", "down"]: self._update_one_side(side) return self def _update_one_side(self, side: str): - if side == 'up': + if side == "up": self.history_peaks[side] = heapq.nlargest( self.num_threshold[side], list(self.window) + self.history_peaks[side], @@ -284,9 +284,9 @@ def _update_one_side(self, side: str): self.peaks[side] = np.array(self.history_peaks[side]) - np.array( self.init_threshold[side] ) - elif side == 'down': + elif side == "down": self.history_peaks[side] = heapq.nsmallest( - self.num_threshold['down'], + self.num_threshold["down"], list(self.window) + self.history_peaks[side], ) self.init_threshold[side] = self.history_peaks[side][-1] @@ -312,7 +312,6 @@ def _cal_back_mean(self, data): def fit(self, data: Union[float, int]): - self.back_mean_window.append(data) if self.index >= self.back_mean_len: @@ -333,25 +332,27 @@ def fit(self, data: Union[float, int]): if ( abs( np.divide( - data - last_data, last_data, np.array(data), where=last_data != 0 + data - last_data, + last_data, + np.array(data), + where=last_data != 0, ) ) < self.deviance_ratio ): return self - if self.normal_X > self.init_threshold['up']: - self._update_one_side('up') + if self.normal_X > self.init_threshold["up"]: + self._update_one_side("up") - elif self.normal_X < self.init_threshold['down']: + elif self.normal_X < self.init_threshold["down"]: - self._update_one_side('down') + self._update_one_side("down") return self def score(self, data: Union[float, int]) -> float: - last_data = ( self.window[-2] if self.back_mean_len == 0 @@ -359,19 +360,26 @@ def score(self, data: Union[float, int]) -> float: ) if ( - abs(np.divide(data - last_data, last_data, np.array(data), where=last_data != 0)) + abs( + np.divide( + data - last_data, + last_data, + np.array(data), + where=last_data != 0, + ) + ) < self.deviance_ratio ): score = 0.0 elif ( - self.normal_X > self.extreme_quantile['up'] - or self.normal_X < self.extreme_quantile['down'] + self.normal_X > self.extreme_quantile["up"] + or self.normal_X < self.extreme_quantile["down"] ): score = 1.0 - elif self.normal_X > self.init_threshold['up']: - side = 'up' + elif self.normal_X > self.init_threshold["up"]: + side = "up" score = np.divide( self.normal_X - self.init_threshold[side], (self.extreme_quantile[side] - self.init_threshold[side]), @@ -381,8 +389,8 @@ def score(self, data: Union[float, int]) -> float: ), ) - elif self.normal_X < self.init_threshold['down']: - side = 'down' + elif self.normal_X < self.init_threshold["down"]: + side = "down" score = np.divide( self.init_threshold[side] - self.normal_X, (self.init_threshold[side] - self.extreme_quantile[side]), diff --git a/engine/models/metric/serve/deployment.py b/engine/models/metric/serve/deployment.py new file mode 100644 index 0000000..6387e6e --- /dev/null +++ b/engine/models/metric/serve/deployment.py @@ -0,0 +1,58 @@ +# Copyright 2022 SkyAPM org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ray + + +@ray.remote(num_cpus=1) +class RayMetricConsumer(object): + def __init__(self, detector) -> None: + self.detector = detector + + def run(self, timestamp: str, data: float) -> float: + score = self.detector.fit_score(timestamp=timestamp, data=float(data)) + return score + + +if __name__ == "__main__": + + from engine.models.metric.detectors import SpotDetector + import pandas as pd + + df = pd.read_csv( + "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" + ) + + # This for loop can be replaced by a MQ + for index, row in df.iterrows(): + timestamp = index + data = row.value + + # Here we setup multi-consumer according to unique name + detector = RayMetricConsumer.options( + name="cpu.load", lifetime="detached", get_if_exists=True + ).remote(SpotDetector()) + + score1 = ray.get( + detector.run.remote(timestamp=timestamp, data=float(data)) + ) + + detector = RayMetricConsumer.options( + name="mem.load", lifetime="detached", get_if_exists=True + ).remote(SpotDetector()) + score2 = ray.get( + detector.run.remote(timestamp=timestamp, data=float(data)) + ) + + assert score1 == score2 diff --git a/engine/models/tests/test_detector.py b/engine/models/tests/test_detector.py index b1228e5..620643e 100644 --- a/engine/models/tests/test_detector.py +++ b/engine/models/tests/test_detector.py @@ -13,24 +13,55 @@ # limitations under the License. - import sys import pandas as pd from engine.models.metric.detectors import SpotDetector +from engine.models.metric.serve.deployment import RayMetricConsumer +import ray -sys.path.append('../../') +sys.path.append("../../") def test_detector_uni_dataset(): df = pd.read_csv( - 'experiments/metric/data/univarate_dataset.csv', index_col='timestamp' + "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" ) - detector = SpotDetector() + detector = SpotDetector.remote() for index, row in df.iterrows(): timestamp = index data = row.value - score = detector.fit_score(timestamp=timestamp, data=float(data)) + + score = ray.get( + detector.fit_score.remote(timestamp=timestamp, data=float(data)) + ) assert score is None or 0 <= score <= 1 + + +def test_multi_detector_uni_dataset(): + df = pd.read_csv( + "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" + ) + + for index, row in df.iterrows(): + timestamp = index + data = row.value + + detector = RayMetricConsumer.options( + name="cpu.load", lifetime="detached", get_if_exists=True + ).remote(SpotDetector()) + + score1 = ray.get( + detector.run.remote(timestamp=timestamp, data=float(data)) + ) + + detector = RayMetricConsumer.options( + name="mem.load", lifetime="detached", get_if_exists=True + ).remote(SpotDetector()) + score2 = ray.get( + detector.run.remote(timestamp=timestamp, data=float(data)) + ) + + assert score1 == score2 From b5008d9d3f0d15bf258fedefd3122b98d9b085d9 Mon Sep 17 00:00:00 2001 From: Fengrui-Liu Date: Sun, 6 Nov 2022 19:58:35 +0800 Subject: [PATCH 2/3] Fix: quota formatter --- .../models/metric/detectors/spot_detector.py | 60 +++++++++---------- engine/models/metric/serve/deployment.py | 12 ++-- engine/models/tests/test_detector.py | 14 ++--- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/engine/models/metric/detectors/spot_detector.py b/engine/models/metric/detectors/spot_detector.py index 8e2449e..2829fb1 100644 --- a/engine/models/metric/detectors/spot_detector.py +++ b/engine/models/metric/detectors/spot_detector.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, +# distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @@ -22,7 +22,7 @@ from ..base import BaseDetector -np.seterr(divide="ignore", invalid="ignore") +np.seterr(divide='ignore', invalid='ignore') class SpotDetector(BaseDetector): @@ -53,19 +53,19 @@ def __init__( self.back_mean_window = deque(maxlen=self.back_mean_len) assert ( self.window_len > 100 - ), "window_len is too small, default value is 200" + ), 'window_len is too small, default value is 200' self.num_threshold = { - "up": num_threshold_up, - "down": num_threshold_down, + 'up': num_threshold_up, + 'down': num_threshold_down, } - nonedict = {"up": None, "down": None} + nonedict = {'up': None, 'down': None} self.extreme_quantile = dict.copy(nonedict) self.init_threshold = dict.copy(nonedict) self.peaks = dict.copy(nonedict) - self.history_peaks = {"up": [], "down": []} + self.history_peaks = {'up': [], 'down': []} # self.peaks = {'up':deque(maxlen=20),'down':deque(maxlen=20)} self.gamma = dict.copy(nonedict) self.sigma = dict.copy(nonedict) @@ -130,7 +130,7 @@ def jac_w(y, t): lambda t: jac_w(self.peaks[side], t), (d, e) if d < e else (e, d), n_points, - "regular", + 'regular', ) right_zeros = self._roots_finder( @@ -138,7 +138,7 @@ def jac_w(y, t): lambda t: jac_w(self.peaks[side], t), (b, c) if b < c else (c, b), n_points, - "regular", + 'regular', ) # all the possible roots @@ -183,13 +183,13 @@ def _roots_finder(self, fun, jac, bounds, npoints, method): numpy.array possible roots of the function """ - if method == "regular": + if method == 'regular': step = (bounds[1] - bounds[0]) / (npoints + 1) try: x_sampled = np.arange(bounds[0] + step, bounds[1], step) except BaseException: x_sampled = np.random.uniform(bounds[0], bounds[1], npoints) - elif method == "random": + elif method == 'random': x_sampled = np.random.uniform(bounds[0], bounds[1], npoints) def obj_fun(x_sampled, f, jac): @@ -206,7 +206,7 @@ def obj_fun(x_sampled, f, jac): opt = minimize( lambda X: obj_fun(X, fun, jac), x_sampled, - method="L-BFGS-B", + method='L-BFGS-B', jac=True, bounds=[bounds] * len(x_sampled), ) @@ -245,7 +245,7 @@ def _log_likelihood(self, y, gamma, sigma): def _quantile(self, side, gamma, sigma): - if side == "up": + if side == 'up': r = self.window_len * self.prob / self.num_threshold[side] if gamma != 0: @@ -254,7 +254,7 @@ def _quantile(self, side, gamma, sigma): ) else: return self.init_threshold[side] - sigma * log(r) - elif side == "down": + elif side == 'down': r = self.window_len * self.prob / self.num_threshold[side] if gamma != 0: @@ -264,18 +264,18 @@ def _quantile(self, side, gamma, sigma): else: return self.init_threshold[side] + sigma * log(r) else: - raise ValueError("The side is not right") + raise ValueError('The side is not right') def _init_drift(self, verbose=False): - for side in ["up", "down"]: + for side in ['up', 'down']: self._update_one_side(side) return self def _update_one_side(self, side: str): - if side == "up": + if side == 'up': self.history_peaks[side] = heapq.nlargest( self.num_threshold[side], list(self.window) + self.history_peaks[side], @@ -284,9 +284,9 @@ def _update_one_side(self, side: str): self.peaks[side] = np.array(self.history_peaks[side]) - np.array( self.init_threshold[side] ) - elif side == "down": + elif side == 'down': self.history_peaks[side] = heapq.nsmallest( - self.num_threshold["down"], + self.num_threshold['down'], list(self.window) + self.history_peaks[side], ) self.init_threshold[side] = self.history_peaks[side][-1] @@ -342,12 +342,12 @@ def fit(self, data: Union[float, int]): ): return self - if self.normal_X > self.init_threshold["up"]: - self._update_one_side("up") + if self.normal_X > self.init_threshold['up']: + self._update_one_side('up') - elif self.normal_X < self.init_threshold["down"]: + elif self.normal_X < self.init_threshold['down']: - self._update_one_side("down") + self._update_one_side('down') return self @@ -373,13 +373,13 @@ def score(self, data: Union[float, int]) -> float: score = 0.0 elif ( - self.normal_X > self.extreme_quantile["up"] - or self.normal_X < self.extreme_quantile["down"] + self.normal_X > self.extreme_quantile['up'] + or self.normal_X < self.extreme_quantile['down'] ): score = 1.0 - elif self.normal_X > self.init_threshold["up"]: - side = "up" + elif self.normal_X > self.init_threshold['up']: + side = 'up' score = np.divide( self.normal_X - self.init_threshold[side], (self.extreme_quantile[side] - self.init_threshold[side]), @@ -389,8 +389,8 @@ def score(self, data: Union[float, int]) -> float: ), ) - elif self.normal_X < self.init_threshold["down"]: - side = "down" + elif self.normal_X < self.init_threshold['down']: + side = 'down' score = np.divide( self.init_threshold[side] - self.normal_X, (self.init_threshold[side] - self.extreme_quantile[side]), diff --git a/engine/models/metric/serve/deployment.py b/engine/models/metric/serve/deployment.py index 6387e6e..a78dee2 100644 --- a/engine/models/metric/serve/deployment.py +++ b/engine/models/metric/serve/deployment.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, +# distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @@ -25,13 +25,13 @@ def run(self, timestamp: str, data: float) -> float: return score -if __name__ == "__main__": +if __name__ == '__main__': from engine.models.metric.detectors import SpotDetector import pandas as pd df = pd.read_csv( - "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" + 'experiments/metric/data/univarate_dataset.csv', index_col='timestamp' ) # This for loop can be replaced by a MQ @@ -41,7 +41,7 @@ def run(self, timestamp: str, data: float) -> float: # Here we setup multi-consumer according to unique name detector = RayMetricConsumer.options( - name="cpu.load", lifetime="detached", get_if_exists=True + name='cpu.load', lifetime='detached', get_if_exists=True ).remote(SpotDetector()) score1 = ray.get( @@ -49,7 +49,7 @@ def run(self, timestamp: str, data: float) -> float: ) detector = RayMetricConsumer.options( - name="mem.load", lifetime="detached", get_if_exists=True + name='mem.load', lifetime='detached', get_if_exists=True ).remote(SpotDetector()) score2 = ray.get( detector.run.remote(timestamp=timestamp, data=float(data)) diff --git a/engine/models/tests/test_detector.py b/engine/models/tests/test_detector.py index 620643e..86f4f48 100644 --- a/engine/models/tests/test_detector.py +++ b/engine/models/tests/test_detector.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, +# distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @@ -21,12 +21,12 @@ from engine.models.metric.serve.deployment import RayMetricConsumer import ray -sys.path.append("../../") +sys.path.append('../../') def test_detector_uni_dataset(): df = pd.read_csv( - "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" + 'experiments/metric/data/univarate_dataset.csv', index_col='timestamp' ) detector = SpotDetector.remote() @@ -42,7 +42,7 @@ def test_detector_uni_dataset(): def test_multi_detector_uni_dataset(): df = pd.read_csv( - "experiments/metric/data/univarate_dataset.csv", index_col="timestamp" + 'experiments/metric/data/univarate_dataset.csv', index_col='timestamp' ) for index, row in df.iterrows(): @@ -50,7 +50,7 @@ def test_multi_detector_uni_dataset(): data = row.value detector = RayMetricConsumer.options( - name="cpu.load", lifetime="detached", get_if_exists=True + name='cpu.load', lifetime='detached', get_if_exists=True ).remote(SpotDetector()) score1 = ray.get( @@ -58,7 +58,7 @@ def test_multi_detector_uni_dataset(): ) detector = RayMetricConsumer.options( - name="mem.load", lifetime="detached", get_if_exists=True + name='mem.load', lifetime='detached', get_if_exists=True ).remote(SpotDetector()) score2 = ray.get( detector.run.remote(timestamp=timestamp, data=float(data)) From 7ae1e0ee049f7c494eeec870da28489503cda51f Mon Sep 17 00:00:00 2001 From: Fengrui-Liu Date: Sun, 6 Nov 2022 20:22:47 +0800 Subject: [PATCH 3/3] Fix: quota formatter --- engine/models/metric/detectors/spot_detector.py | 4 ++-- engine/models/metric/serve/deployment.py | 4 ++-- engine/models/tests/test_detector.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/engine/models/metric/detectors/spot_detector.py b/engine/models/metric/detectors/spot_detector.py index 2829fb1..ce4cd02 100644 --- a/engine/models/metric/detectors/spot_detector.py +++ b/engine/models/metric/detectors/spot_detector.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/engine/models/metric/serve/deployment.py b/engine/models/metric/serve/deployment.py index a78dee2..76a3cb6 100644 --- a/engine/models/metric/serve/deployment.py +++ b/engine/models/metric/serve/deployment.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/engine/models/tests/test_detector.py b/engine/models/tests/test_detector.py index 86f4f48..a669676 100644 --- a/engine/models/tests/test_detector.py +++ b/engine/models/tests/test_detector.py @@ -1,13 +1,13 @@ # Copyright 2022 SkyAPM org # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.