-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcvxpy_continuous_solver.py
More file actions
174 lines (159 loc) · 6.86 KB
/
cvxpy_continuous_solver.py
File metadata and controls
174 lines (159 loc) · 6.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import cvxpy as cp
import numpy as np
from rich import print as rprint
import time
# solves the Sia linear relaxation using cvxpy
# separable form: min sum_i c_i^T x_i,
# s.t. G sum_i x_i <= g
# 0 <= x_i <= 1
# standard form: min c^T x,
# s.t. Ax <= b
class SiaCvxpyContinuousSolver:
# initialize solver
def __init__(self, params):
self.G_mat = params['G_mat']
self.g_vec = params['g_vec']
self.solver_name = params['solver_name']
self.rtol = params.get('rtol', 1e-6)
self.solver_map = {'GLPK': cp.GLPK, 'CBC': cp.CBC, 'SCS': cp.SCS, 'PROXQP': cp.PROXQP, 'PIQP' : cp.PIQP}
self.solver_params = {'GLPK': {'verbose':True,
'opts':{'msg_lev': 'GLP_MSG_OFF', 'tol_bnd': self.rtol}},
'CBC': {'verbose':True, 'allowableGap': self.rtol*100},
'SCS': {'verbose':True, 'eps': self.rtol},
'PROXQP': {'verbose':True, 'eps_rel': self.rtol},
'PIQP': {'verbose':True, 'eps_rel': self.rtol}}
# problem parameters
self.num_configs = self.G_mat.shape[1]
self.num_jobs = 0
self.num_gpu_types = self.G_mat.shape[0]
self.num_variables = self.num_jobs * self.num_configs
# jobname -> idx mapping
self.job_to_idx = dict()
# state for the problem
self.cost_vector = None
self.A_mat = None
self.b_vec = None
self.variables = None
self.problem = None
self.constraints = None
self.solution = None
self.results = None
self.is_solved = False
def __reconstruct_standard_form(self):
rprint(f"Reconstructing standard form for {self.num_jobs} jobs")
self.num_variables = self.num_jobs * self.num_configs
num_rows = self.num_gpu_types + self.num_jobs + (2 * self.num_variables)
num_cols = self.num_jobs * self.num_configs
new_A_mat = np.zeros(shape=(num_rows, num_cols))
new_b_vec = np.zeros(shape=(num_rows, ))
# Gx <= g constraints
new_A_mat[:self.num_gpu_types, :] = np.tile(self.G_mat, (1, self.num_jobs))
new_b_vec[:self.num_gpu_types] = self.g_vec
# sum-to-1 constraints
start_idx = self.num_gpu_types
for job_idx in range(self.num_jobs):
new_A_mat[start_idx, (job_idx*self.num_configs) : (job_idx + 1)*self.num_configs] = 1
new_b_vec[start_idx] = 1
start_idx += 1
# 0 <= x_i constraints ==> represented as (-x_i <= 0)
new_A_mat[start_idx: start_idx + self.num_variables, :] = -np.eye(self.num_variables)
new_b_vec[start_idx: start_idx + self.num_variables] = 0
start_idx += self.num_variables
# x_i <= 1 constraints
new_A_mat[start_idx: start_idx + self.num_variables, :] = np.eye(self.num_variables)
new_b_vec[start_idx: start_idx + self.num_variables] = 1
# update the standard form matrices
self.A_mat = new_A_mat
self.b_vec = new_b_vec
# reconstruct the CVXPY problem
self.variables = cp.Variable(self.num_variables)
self.variables.value = self.solution
self.constraints = [self.A_mat @ self.variables <= self.b_vec]
self.problem = cp.Problem(cp.Minimize(self.cost_vector @ self.variables), self.constraints)
def update_costs(self, cost_updates):
self.is_solved = False
for jobname in cost_updates:
job_idx = self.job_to_idx.get(jobname, None)
if job_idx is None:
rprint(f"Job {jobname} not found in job_to_idx mapping")
continue
cost_vec_start_idx = job_idx * self.num_configs
self.cost_vector[cost_vec_start_idx: cost_vec_start_idx+self.num_configs] = cost_updates[jobname]
# added_jobs = {new_jobname: cost_vector}
# removed_jobs = [jobname]
def update_jobs(self, added_jobs, removed_jobs):
# no changes to set of jobs
if len(added_jobs) == 0 and len(removed_jobs) == 0:
rprint(f"No changes to job list")
return
# needs to be solved again
self.is_solved = False
# remove all removed jobs
removed_jobs = [x for x in removed_jobs if x in self.job_to_idx]
for jobname in removed_jobs:
# remove job from job_to_idx mapping
self.job_to_idx.pop(jobname)
# reassign indices for jobs
new_num_jobs = self.num_jobs + len(added_jobs) - len(removed_jobs)
must_reconstruct_standard_form = (new_num_jobs != self.num_jobs)
new_num_variables = new_num_jobs * self.num_configs
new_cost_vector = np.zeros(shape=(new_num_variables, ))
new_solution_vector = np.zeros(shape=(new_num_variables, ))
new_job_to_idx = dict()
cur_idx = 0
for jobname, idx in self.job_to_idx.items():
new_job_to_idx[jobname] = cur_idx
# copy over cost vector
src_start_idx = idx * self.num_configs
dest_start_idx = cur_idx * self.num_configs
new_cost_vector[dest_start_idx: dest_start_idx+self.num_configs] = self.cost_vector[src_start_idx: src_start_idx+self.num_configs]
# copy over solution vector
new_solution_vector[dest_start_idx: dest_start_idx+self.num_configs] = self.solution[src_start_idx: src_start_idx+self.num_configs]
# bump up index
cur_idx += 1
# add new jobs
for jobname, cost_vector in added_jobs.items():
new_job_to_idx[jobname] = cur_idx
dest_start_idx = cur_idx * self.num_configs
new_cost_vector[dest_start_idx: dest_start_idx+self.num_configs] = cost_vector
# bump up index
cur_idx += 1
# update job_to_idx mapping
self.job_to_idx = new_job_to_idx
# update cost vector, solution vectors (initialized to 0 for new jobs)
self.cost_vector = new_cost_vector
self.solution = new_solution_vector
# update number of jobs and variables; reconstruct standard form if needed
self.num_jobs = new_num_jobs
self.num_variables = new_num_variables
if must_reconstruct_standard_form:
self.__reconstruct_standard_form()
# solve the existing problem
def solve(self):
solver = self.solver_map.get(self.solver_name, None)
addnl_params = self.solver_params.get(self.solver_name, {})
# return if already solved
if self.is_solved:
return self.results
else:
start_t = time.time()
self.problem.solve(solver=solver, **addnl_params)
end_t = time.time()
self.is_solved = True
self.results = {'status': self.problem.status,
'optimal_value': self.problem.value,
'solver_time_ms': (end_t - start_t)*1000.0}
self.solution = self.variables.value
return self.results
# returns optimal solution to last solved problem
def get_solution(self, jobname=None):
if not self.is_solved:
rprint(f"Problem not solved yet")
return None
chosen_jobs = list(self.job_to_idx.keys()) if jobname is None else [jobname]
solutions = dict()
for jobname in chosen_jobs:
job_idx = self.job_to_idx.get(jobname, None)
start_idx = job_idx * self.num_configs
solutions[jobname] = self.solution[start_idx: start_idx+self.num_configs]
return solutions