1
1
import unittest
2
- from unittest .mock import patch , Mock , mock_open
2
+ from unittest .mock import patch , Mock , mock_open , call
3
3
import yaml
4
4
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
6
6
7
7
TEST_CONFIG_DATA = {
8
- "vLLMLoRAConfig" : {
9
- "name" : "test-deployment" ,
8
+ BASE_FIELD : {
10
9
"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
+ },
21
50
}
22
51
}
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
+ ]
24
61
RESPONSES = {
25
62
"v1/models" : {
26
63
"object" : "list" ,
27
64
"data" : [
28
65
{
29
- "id" : "base1 " ,
66
+ "id" : "already_exists " ,
30
67
"object" : "model" ,
31
68
"created" : 1729693000 ,
32
69
"owned_by" : "vllm" ,
35
72
"max_model_len" : 4096 ,
36
73
},
37
74
{
38
- "id" : "lora2 " ,
75
+ "id" : "to_remove " ,
39
76
"object" : "model" ,
40
77
"created" : 1729693000 ,
41
78
"owned_by" : "vllm" ,
46
83
],
47
84
},
48
85
}
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
- }
69
86
70
87
71
88
def getMockResponse (status_return_value : object = None ) -> object :
@@ -79,80 +96,91 @@ class LoraReconcilerTest(unittest.TestCase):
79
96
"builtins.open" , new_callable = mock_open , read_data = yaml .dump (TEST_CONFIG_DATA )
80
97
)
81
98
@patch ("sidecar.requests.get" )
82
- @patch .dict (os .environ , {DYNAMIC_LORA_FLAG : "test_api_key" })
83
99
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 ):
85
101
mock_response = getMockResponse ()
86
102
mock_response .json .return_value = RESPONSES ["v1/models" ]
87
103
mock_get .return_value = mock_response
88
104
self .reconciler = LoraReconciler ()
89
105
self .maxDiff = None
90
- mock_file .assert_called_once_with (CONFIG_MAP_FILE , "r" )
91
106
92
107
@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
-
102
108
@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 )
112
130
131
+ @patch ("sidecar.requests.get" )
113
132
@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
+ )
124
151
125
152
@patch (
126
153
"builtins.open" , new_callable = mock_open , read_data = yaml .dump (TEST_CONFIG_DATA )
127
154
)
128
155
@patch ("sidecar.requests.get" )
129
156
@patch ("sidecar.requests.post" )
130
157
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
156
184
157
185
if __name__ == "__main__" :
158
186
unittest .main ()
0 commit comments