Skip to content

Commit 65cea88

Browse files
committed
Modify unittests
Signed-off-by: Kunjan Patel <[email protected]>
1 parent 3140610 commit 65cea88

File tree

4 files changed

+130
-102
lines changed

4 files changed

+130
-102
lines changed

examples/dynamic-lora-sidecar/deployment.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ spec:
7272
env:
7373
- name: DYNAMIC_LORA_ROLLOUT_CONFIG
7474
value: "/config/configmap.yaml"
75-
volumeMounts:
75+
volumeMounts: # DO NOT USE subPath
7676
- name: config-volume
7777
mountPath: /config
7878
volumes:
@@ -110,7 +110,7 @@ data:
110110
vLLMLoRAConfig:
111111
host: localhost
112112
name: sql-loras-llama
113-
port: '8000'
113+
port: 8000
114114
ensureExist:
115115
models:
116116
- base-model: meta-llama/Llama-2-7b-hf

examples/dynamic-lora-sidecar/sidecar/configmap.yaml

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ vLLMLoRAConfig:
44
port: 8000
55
ensureExist:
66
models:
7-
- base-model: meta-llama/Llama-2-7b-hf
7+
- base-model: meta-llama/Llama-2-7b-hf #optional
88
id: sql-lora-v1
99
source: yard1/llama-2-7b-sql-lora-test
1010
- base-model: meta-llama/Llama-2-7b-hf
11-
id: sql-lora-v5
12-
source: yard1/llama-2-7b-sql-lora-test
13-
- base-model: meta-llama/Llama-2-7b-hf
14-
id: sql-lora-v6
11+
id: sql-lora-v3
1512
source: yard1/llama-2-7b-sql-lora-test
1613
ensureNotExist:
1714
models:
1815
- base-model: meta-llama/Llama-2-7b-hf
1916
id: sql-lora-v2
17+
source: yard1/llama-2-7b-sql-lora-test
18+
- base-model: meta-llama/Llama-2-7b-hf
19+
id: sql-lora-v3
2020
source: yard1/llama-2-7b-sql-lora-test

examples/dynamic-lora-sidecar/sidecar/sidecar.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import yaml
33
import time
44
from watchfiles import awatch
5+
from dataclasses import dataclass
56
import asyncio
67
import logging
78
import datetime
@@ -21,7 +22,7 @@ def current_time_human() -> str:
2122
now = datetime.datetime.now(datetime.timezone.utc).astimezone()
2223
return now.strftime("%Y-%m-%d %H:%M:%S %Z%z")
2324

24-
25+
@dataclass
2526
class LoraAdapter:
2627
"""Class representation of lora adapters in config"""
2728
def __init__(self, id, source="", base_model=""):
@@ -187,7 +188,6 @@ async def main():
187188
reconcilerInstance = LoraReconciler()
188189
logging.info(f"running reconcile for initial loading of configmap {CONFIG_MAP_FILE}")
189190
reconcilerInstance.reconcile()
190-
# observer = Observer()
191191
logging.info(f"beginning watching of configmap {CONFIG_MAP_FILE}")
192192
async for _ in awatch('/config/configmap.yaml'):
193193
logging.info(f"Config '{CONFIG_MAP_FILE}' modified!'" )
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,69 @@
11
import unittest
2-
from unittest.mock import patch, Mock, mock_open
2+
from unittest.mock import patch, Mock, mock_open, call
33
import yaml
44
import os
5-
from sidecar import LoraReconciler, CONFIG_MAP_FILE, BASE_FIELD, DYNAMIC_LORA_FLAG
5+
from sidecar import LoraReconciler, CONFIG_MAP_FILE, BASE_FIELD, LoraAdapter
66

77
TEST_CONFIG_DATA = {
8-
"vLLMLoRAConfig": {
9-
"name": "test-deployment",
8+
BASE_FIELD: {
109
"host": "localhost",
11-
"port": "8000",
12-
"models": [
13-
{"id": "lora1", "source": "/path/to/lora1", "base-model": "base1"},
14-
{
15-
"id": "lora2",
16-
"source": "/path/to/lora2",
17-
"base-model": "base1",
18-
"toRemove": True,
19-
},
20-
],
10+
"name": "sql-loras-llama",
11+
"port": 8000,
12+
"ensureExist": {
13+
"models": [
14+
{
15+
"base-model": "meta-llama/Llama-2-7b-hf",
16+
"id": "sql-lora-v1",
17+
"source": "yard1/llama-2-7b-sql-lora-test",
18+
},
19+
{
20+
"base-model": "meta-llama/Llama-2-7b-hf",
21+
"id": "sql-lora-v3",
22+
"source": "yard1/llama-2-7b-sql-lora-test",
23+
},
24+
{
25+
"base-model": "meta-llama/Llama-2-7b-hf",
26+
"id": "already_exists",
27+
"source": "yard1/llama-2-7b-sql-lora-test",
28+
},
29+
]
30+
},
31+
"ensureNotExist": {
32+
"models": [
33+
{
34+
"base-model": "meta-llama/Llama-2-7b-hf",
35+
"id": "sql-lora-v2",
36+
"source": "yard1/llama-2-7b-sql-lora-test",
37+
},
38+
{
39+
"base-model": "meta-llama/Llama-2-7b-hf",
40+
"id": "sql-lora-v3",
41+
"source": "yard1/llama-2-7b-sql-lora-test",
42+
},
43+
{
44+
"base-model": "meta-llama/Llama-2-7b-hf",
45+
"id": "to_remove",
46+
"source": "yard1/llama-2-7b-sql-lora-test",
47+
},
48+
]
49+
},
2150
}
2251
}
23-
52+
EXIST_ADAPTERS = [
53+
LoraAdapter(a["id"], a["base-model"], a["source"])
54+
for a in TEST_CONFIG_DATA[BASE_FIELD]["ensureExist"]["models"]
55+
]
56+
57+
NOT_EXIST_ADAPTERS = [
58+
LoraAdapter(a["id"], a["base-model"], a["source"])
59+
for a in TEST_CONFIG_DATA[BASE_FIELD]["ensureNotExist"]["models"]
60+
]
2461
RESPONSES = {
2562
"v1/models": {
2663
"object": "list",
2764
"data": [
2865
{
29-
"id": "base1",
66+
"id": "already_exists",
3067
"object": "model",
3168
"created": 1729693000,
3269
"owned_by": "vllm",
@@ -35,7 +72,7 @@
3572
"max_model_len": 4096,
3673
},
3774
{
38-
"id": "lora2",
75+
"id": "to_remove",
3976
"object": "model",
4077
"created": 1729693000,
4178
"owned_by": "vllm",
@@ -46,26 +83,6 @@
4683
],
4784
},
4885
}
49-
REGISTERED_ADAPTERS = {
50-
"base1": {
51-
"created": 1729693000,
52-
"id": "base1",
53-
"max_model_len": 4096,
54-
"object": "model",
55-
"owned_by": "vllm",
56-
"parent": None,
57-
"root": "meta-llama/Llama-2-7b-hf",
58-
},
59-
"lora2": {
60-
"created": 1729693000,
61-
"id": "lora2",
62-
"max_model_len": None,
63-
"object": "model",
64-
"owned_by": "vllm",
65-
"parent": "base1",
66-
"root": "yard1/llama-2-7b-sql-lora-test",
67-
},
68-
}
6986

7087

7188
def getMockResponse(status_return_value: object = None) -> object:
@@ -79,80 +96,91 @@ class LoraReconcilerTest(unittest.TestCase):
7996
"builtins.open", new_callable=mock_open, read_data=yaml.dump(TEST_CONFIG_DATA)
8097
)
8198
@patch("sidecar.requests.get")
82-
@patch.dict(os.environ, {DYNAMIC_LORA_FLAG: "test_api_key"})
8399
def setUp(self, mock_get, mock_file):
84-
with patch.object(LoraReconciler, "check_health", return_value=True):
100+
with patch.object(LoraReconciler, "is_server_healthy", return_value=True):
85101
mock_response = getMockResponse()
86102
mock_response.json.return_value = RESPONSES["v1/models"]
87103
mock_get.return_value = mock_response
88104
self.reconciler = LoraReconciler()
89105
self.maxDiff = None
90-
mock_file.assert_called_once_with(CONFIG_MAP_FILE, "r")
91106

92107
@patch("sidecar.requests.get")
93-
def test_get_registered_adapters(self, mock_get):
94-
with patch.object(LoraReconciler, "check_health", return_value=True):
95-
mock_response = getMockResponse()
96-
mock_response.json.return_value = RESPONSES["v1/models"]
97-
mock_get.return_value = mock_response
98-
99-
self.reconciler.get_registered_adapters()
100-
self.assertEqual(REGISTERED_ADAPTERS, self.reconciler.registered_adapters)
101-
102108
@patch("sidecar.requests.post")
103-
def test_load_adapter(self, mock_post):
104-
with patch.object(LoraReconciler, "check_health", return_value=True):
105-
mock_post.return_value = getMockResponse()
106-
107-
# loading a new adapter
108-
result = self.reconciler.load_adapter(
109-
TEST_CONFIG_DATA[BASE_FIELD]["models"][0]
110-
)
111-
self.assertEqual(result, "")
109+
def test_load_adapter(self, mock_post: Mock, mock_get: Mock):
110+
mock_response = getMockResponse()
111+
mock_response.json.return_value = RESPONSES["v1/models"]
112+
mock_get.return_value = mock_response
113+
mock_file = mock_open(read_data=yaml.dump(TEST_CONFIG_DATA))
114+
with patch("builtins.open", mock_file):
115+
with patch.object(LoraReconciler, "is_server_healthy", return_value=True):
116+
mock_post.return_value = getMockResponse()
117+
# loading a new adapter
118+
adapter = EXIST_ADAPTERS[0]
119+
url = "http://localhost:8000/v1/load_lora_adapter"
120+
payload = {
121+
"lora_name": adapter.id,
122+
"lora_path": adapter.source,
123+
"base_model_name": adapter.base_model,
124+
}
125+
self.reconciler.load_adapter(adapter)
126+
# adapter 2 already exists `id:already_exists`
127+
already_exists = EXIST_ADAPTERS[2]
128+
self.reconciler.load_adapter(already_exists)
129+
mock_post.assert_called_once_with(url, json=payload)
112130

131+
@patch("sidecar.requests.get")
113132
@patch("sidecar.requests.post")
114-
def test_unload_adapter(self, mock_post):
115-
with patch.object(LoraReconciler, "check_health", return_value=True):
116-
mock_post.return_value = getMockResponse()
117-
118-
# unloading an existing adapter
119-
self.reconciler.registered_adapters["lora2"] = {"id": "lora2"}
120-
result = self.reconciler.unload_adapter(
121-
TEST_CONFIG_DATA[BASE_FIELD]["models"][1]
122-
)
123-
self.assertEqual(result, None)
133+
def test_unload_adapter(self, mock_post: Mock, mock_get: Mock):
134+
mock_response = getMockResponse()
135+
mock_response.json.return_value = RESPONSES["v1/models"]
136+
mock_get.return_value = mock_response
137+
mock_file = mock_open(read_data=yaml.dump(TEST_CONFIG_DATA))
138+
with patch("builtins.open", mock_file):
139+
with patch.object(LoraReconciler, "is_server_healthy", return_value=True):
140+
mock_post.return_value = getMockResponse()
141+
# unloading an existing adapter `id:to_remove`
142+
adapter = NOT_EXIST_ADAPTERS[2]
143+
self.reconciler.unload_adapter(adapter)
144+
payload = {"lora_name": adapter.id}
145+
adapter = NOT_EXIST_ADAPTERS[0]
146+
self.reconciler.unload_adapter(adapter)
147+
mock_post.assert_called_once_with(
148+
"http://localhost:8000/v1/unload_lora_adapter",
149+
json=payload,
150+
)
124151

125152
@patch(
126153
"builtins.open", new_callable=mock_open, read_data=yaml.dump(TEST_CONFIG_DATA)
127154
)
128155
@patch("sidecar.requests.get")
129156
@patch("sidecar.requests.post")
130157
def test_reconcile(self, mock_post, mock_get, mock_file):
131-
with patch.object(LoraReconciler, "check_health", return_value=True):
132-
mock_get_response = getMockResponse()
133-
mock_get_response.json.return_value = RESPONSES["v1/models"]
134-
mock_get.return_value = mock_get_response
135-
mock_post.return_value = getMockResponse()
136-
137-
self.reconciler = LoraReconciler()
138-
self.reconciler.reconcile()
139-
140-
mock_post.assert_any_call(
141-
"http://localhost:8000/v1/load_lora_adapter",
142-
json={
143-
"lora_name": "lora1",
144-
"lora_path": "/path/to/lora1",
145-
"base_model_name": "base1",
146-
},
147-
)
148-
mock_post.assert_any_call(
149-
"http://localhost:8000/v1/unload_lora_adapter", json={"lora_name": "lora2"}
150-
)
151-
updated_config = self.reconciler.config_map_adapters
152-
mock_file.return_value.write.side_effect = lambda data: data
153-
self.assertTrue("timestamp" in updated_config["lora1"]["status"])
154-
self.assertTrue("status" in updated_config["lora2"])
155-
158+
with patch("builtins.open", mock_file):
159+
with patch.object(LoraReconciler, "is_server_healthy", return_value=True):
160+
with patch.object(
161+
LoraReconciler, "load_adapter", return_value=""
162+
) as mock_load:
163+
with patch.object(
164+
LoraReconciler, "unload_adapter", return_value=""
165+
) as mock_unload:
166+
mock_get_response = getMockResponse()
167+
mock_get_response.json.return_value = RESPONSES["v1/models"]
168+
mock_get.return_value = mock_get_response
169+
mock_post.return_value = getMockResponse()
170+
self.reconciler = LoraReconciler()
171+
self.reconciler.reconcile()
172+
173+
# 1 adapter is in both exist and not exist list, only 2 are expected to be loaded
174+
mock_load.assert_has_calls(
175+
calls=[call(EXIST_ADAPTERS[0]), call(EXIST_ADAPTERS[2])]
176+
)
177+
assert mock_load.call_count == 2
178+
179+
# 1 adapter is in both exist and not exist list, only 2 are expected to be unloaded
180+
mock_unload.assert_has_calls(
181+
calls=[call(NOT_EXIST_ADAPTERS[0]), call(NOT_EXIST_ADAPTERS[2])]
182+
)
183+
assert mock_unload.call_count == 2
156184

157185
if __name__ == "__main__":
158186
unittest.main()

0 commit comments

Comments
 (0)