队列设计

在这个示例中,我们考虑一个 \(M/M/N\) 队列系统的设计,并研究设计对各种参数的敏感性,例如最大总延迟和总服务速率。

这个示例在论文 Differentiating through Log-Log Convex Programs 中有描述。

我们考虑 (Markovian) 队列系统的优化,其中有 \(N\) 个队列。队列系统是一组队列,其中排队的项目等待服务;排队的项目可能是操作系统中的线程,或者是网络系统的输入或输出缓冲区中的数据包。一个自然的目标是在对队列系统的各种属性施加限制的情况下,尽量减少系统的服务负载,例如最大延迟或延迟。在这个示例中,我们将这个设计问题建模为一个对数对数凸规划问题 (LLCP),并计算设计变量对参数的敏感性。在这里考虑的队列系统被称为 \(M/M/N\) 队列。

我们假设到达第 \(i\) 个队列的项目是以速率 \(\lambda_i\) 产生的泊松过程,并且第 \(i\) 个队列的服务时间按参数 \(\mu_i\) 的指数分布进行(其中 \(i=1, \ldots, N\))。队列系统的负载是一个函数 \(\ell: \mathbf{R}^{N}_{++} \times \mathbf{R}^{N}_{++} \to \mathbf{R}^{N}_{++}\),其中包括到达率向量 \(\lambda\) 和服务率向量 \(\mu\),具有以下分量:

\[\ell_i(\lambda, \mu) = \frac{\mu_i}{\lambda_i}, \quad i=1, \ldots, N.\]

(这是流量负载的倒数,通常用 \(\rho\) 表示。)同样,队列占用、平均延迟和系统总延迟分别是函数 \(q\)\(w\)\(d\) 关于 \(\lambda\)\(\mu\) 的函数,其分量为:

\[q_i(\lambda, \mu) = \frac{\ell_i(\lambda, \mu)^{-2}}{1 - \ell_i(\lambda, \mu)^{-1}}, \quad w_i(\lambda, \mu) = \frac{q_i(\lambda, \mu)}{\lambda_i} + \frac{1}{\mu_i}, \quad d_i(\lambda, \mu) = \frac{1}{\mu_i - \lambda_i}这些函数的定义域是 :math:`\{(\lambda, \mu) \in \mathbf{R}^{N}_{++} \times \mathbf{R}^{N}_{++} \mid \lambda < \mu \}`,\]

其中不等式按元素逐个比较。排队系统对队列占用、平均排队延迟和总延迟设置了限制,必须满足以下条件:

\[q(\lambda, \mu) \leq q_{\max}, \quad w(\lambda, \mu) \leq w_{\max}, \quad d(\lambda, \mu) \leq d_{\max},\]

其中 \(q_{\max}\), \(w_{\max}\)\(d_{\max} \in \mathbf{R}^{N}_{++}\) 是参数, 不等式按元素逐个比较。此外,到达率向量 \(\lambda\) 必须至少为 \(\lambda_{\mathrm{min}} \in \mathbf{R}^{N}_{++}\), 服务率之和不能超过 \(\mu_{\max} \in \mathbf{R}_{++}\)

我们的设计问题是选择到达率和服务时间,以最小化服务负载的加权和 \(\gamma^T \ell(\lambda, \mu)\), 其中 \(\gamma \in \mathbf{R}^{N}_{++}\) 是权重向量,同时满足约束条件。该问题的形式如下:

\[\begin{split}\begin{array}{ll} \mbox{minimize} & \gamma^T \ell(\lambda, \mu) \\ \mbox{subject to} & q(\lambda, \mu) \leq q_{\max} \\ & w(\lambda, \mu) \leq w_{\max} \\ & d(\lambda, \mu) \leq d_{\max} \\ & \lambda \geq \lambda_{\mathrm{min}}, \quad \sum_{i=1}^{N} \mu_i \leq \mu_{\max}. \end{array}\end{split}\]

这里,\(\lambda, \mu \in \mathbf{R}^{N}_{++}\) 是变量, \(\gamma, q_{\max}, w_{\max}, d_{\max}, \lambda_{\mathrm{min}} \in \mathbf{R}^{N}_{++}\)\(\mu_{\max} \in \mathbf{R}_{++}\) 是参数。该问题是一个 LLCP 问题。 目标函数是一个多项式,约束函数 \(w\) 也是一个多项式。函数 \(d\)\(q\) 都不是多项式, 但它们是对数对数凸函数;\(d\) 的对数对数凸性由组合规则得出,因为函数 \((x, y) \mapsto y - x\)\(0 < x < y\) 时是对数对数凹函数,而比率函数 \((x, y) \mapsto x/y\) 是对数对数仿射函数, 在 \(y\) 上递减。通过类似的论证,可以证明 \(q\) 也是对数对数凸函数。

import cvxpy as cp
import numpy as np
import time


mu = cp.Variable(pos=True, shape=(2,), name='mu')
lam = cp.Variable(pos=True, shape=(2,), name='lambda')
ell = cp.Variable(pos=True, shape=(2,), name='ell')

w_max = cp.Parameter(pos=True, shape=(2,), value=np.array([2.5, 3.0]), name='w_max')
d_max = cp.Parameter(pos=True, shape=(2,), value=np.array([2., 2.]), name='d_max')
q_max = cp.Parameter(pos=True, shape=(2,), value=np.array([4., 5.0]), name='q_max')
lam_min = cp.Parameter(pos=True, shape=(2,), value=np.array([0.5, 0.8]), name='lambda_min')
mu_max = cp.Parameter(pos=True, value=3.0, name='mu_max')
gamma = cp.Parameter(pos=True, shape=(2,), value=np.array([1.0, 2.0]), name='gamma')

lq = (ell)**(-2)/cp.one_minus_pos(ell**(-1))
q = lq
w = lq/lam + 1/mu
d = 1/cp.diff_pos(mu, lam)

constraints = [
    w <= w_max,
    d <= d_max,
    q <= q_max,
    lam >= lam_min,
    cp.sum(mu) <= mu_max,
    ell == mu/lam,
]

objective_fn = gamma.T @ ell

problem = cp.Problem(cp.Minimize(objective_fn), constraints)
problem.solve(requires_grad=True, gp=True, eps=1e-14, max_iters=10000, mode='dense')
4.457106781186705

解决方案如下。

print('mu ', mu.value)
print('lam ', lam.value)
print('ell ', ell.value)
mu  [1.32842713 1.67157287]
lam  [0.82842712 1.17157287]
ell  [1.60355339 1.4267767 ]

敏感度

我们对每个变量关于参数求导。一个要点是解决方案对于 \(d_{\max}\)\(\mu_{\max}\) 的值非常敏感,增加这些参数会减少服务负载,特别是对于第一个队列。

problem.solve(requires_grad=True, gp=True, eps=1e-14, max_iters=10000, mode='dense')

for var in [lam, mu, ell]:
    print('变量 ', var.name())
    print('对第一个分量的梯度')
    var.gradient = np.array([1., 0.])
    problem.backward()
    for param in problem.parameters():
        if np.prod(param.shape) == 2:
            print('{0}: {1:.3g}, {2:.3g}'.format(param.name(), param.gradient[0], param.gradient[1]))
        else:
            print('{0}: {1:.3g}'.format(param.name(), param.gradient))

    print('对第二个分量的梯度')
    var.gradient = np.array([0., 1.])
    problem.backward()
    for param in problem.parameters():
        if np.prod(param.shape) == 2:
            print('{0}: {1:.3g}, {2:.3g}'.format(param.name(), param.gradient[0], param.gradient[1]))
        else:
            print('{0}: {1:.3g}'.format(param.name(), param.gradient))

    var.gradient = np.zeros(2)
    print('')
变量  lambda
对第一个分量的梯度
gamma: 0.213, -0.107
w_max: 5.43e-12, 5.64e-12
d_max: -0.411, -0.113
q_max: 5.99e-12, 4.77e-12
lambda_min: -1.56e-11, -7.35e-12
mu_max: 0.927
对第二个分量的梯度
gamma: -0.458, 0.229
w_max: 2.08e-11, 2.16e-11
d_max: -0.105, -0.466
q_max: 2.29e-11, 1.83e-11
lambda_min: -5.97e-11, -2.82e-11
mu_max: 1.01

变量  mu
对第一个分量的梯度
gamma: 0.213, -0.107
w_max: 1.55e-11, 1.6e-11
d_max: -0.661, -0.113
q_max: 1.7e-11, 1.36e-11
lambda_min: -4.43e-11, -2.09e-11
mu_max: -0.0727
对第二个分量的梯度
gamma: -0.458, 0.229
w_max: 2.3e-11, 2.39e-11
d_max: -0.105, -0.716
q_max: 2.53e-11, 2.02e-11
lambda_min: -6.59e-11, -3.11e-11
mu_max: 0.00996

变量  ell
对第一个分量的梯度
gamma: -0.245, 0.122
w_max: 2e-11, 2.08e-11
d_max: -0.282, -0.22
q_max: 2.21e-11, 1.76e-11
lambda_min: -5.74e-11, -2.71e-11
mu_max: -0.334
对第二个分量的梯度
gamma: 0.122, -0.0611
w_max: -1.24e-13, -1.29e-13
d_max: -0.101, -0.195
q_max: -1.37e-13, -1.09e-13
lambda_min: 3.58e-13, 1.66e-13
mu_max: -0.197

扰动分析

接下来,我们以一个很小的量对每个参数进行扰动,并将一阶近似预测的解与扰动问题的真实解进行比较。

problem.solve(requires_grad=True, gp=True, eps=1e-14, max_iters=10000, mode='dense')

mu_value = mu.value
lam_value = lam.value

delta = 0.01
for param in problem.parameters():
    param.delta = param.value * delta

problem.derivative()

lam_pred = (lam.delta / lam_value) * 100
mu_pred = (mu.delta / mu_value) * 100

print('预测的 lam(百分比变化):', lam_pred)
print('预测的 mu(百分比变化):', mu_pred)


for param in problem.parameters():
    param._old_value = param.value
    param.value += param.delta
problem.solve(cp.SCS, gp=True, eps=1e-14, max_iters=10000)

lam_actual = ((lam.value - lam_value) / lam_value) * 100
mu_actual = ((mu.value - mu_value) / mu_value) * 100

print('实际的 lam(百分比变化):', lam_actual)
print('实际的 mu(百分比变化):', mu_actual)
预测的 lambda(百分比变化):[2.32203282 1.77228841]
预测的 mu(百分比变化):[1.07166961 0.94304296]
实际的 lambda(百分比变化):[1.99504983 1.99504965]
实际的 mu(百分比变化):[0.87148458 1.10213353]