与负载均衡/重新分配相关的算法名称

7
给定一个数组[x1,x2,x3,...,xk],其中xi是盒子i中物品的数量,如何重新分配物品,以便没有任何一个盒子包含超过N个物品。 N接近于sum(xi)/ k-也就是说,N接近每个盒子具有相同数量的物品。不应使用中间盒子来搬运物品-如果x1有剩余并且x2和x3有赤字,则x1应向x2和x3发送一些物品,但不要将其所有物品都发送到x2,然后让x2解决剩余问题。
实际问题是:每个计算节点都有一组样本,在重新采样步骤之后,某些计算机节点可能会有剩余,而其他节点则存在赤字,因此我希望在最小化通信的同时重新分配样本。
我想象这种问题有一个名称。

1
你的问题描述不清。例如,一打远程节点是否可以同时与其近邻通信,成本为1?如果100个节点有消息要发送到X,它们是否可以同时发送,成本为1,还是必须串行化,成本为100?不同的算法适用于不同的计算模型。特别是,需要说明网络拓扑和/或分布式内存模型。 - James Waldby - jwpat7
这确实没有明确说明,但是下面的几个答案已经提供了很多帮助。 - Gus
7个回答

3

这个问题可以建模为最小费用流的一个实例。

假设有一个有向图G,其中包含顶点s、t、a1、…、ak、b1、…、bk和弧s→ai,其容量为xi,成本为0;弧ai→bj,其容量为无穷大,成本为0(如果i = j)或1(如果i ≠ j);以及弧bj→t,其容量为N,成本为0。我们希望找到从s到t移动∑i xi单位的最小费用流。想法是,如果y单位从ai→bj流动,则将y个物品从箱子i移动到箱子j(如果i ≠ j;如果i = j,则不进行移动)。

当然,使用最小费用流来解决这个简单问题就像用大锤砸蚂蚁一样,但是几个变种可以被建模为网络流问题。

如果一对节点i,j没有直接连接,则删除ai→bj弧。 我们可以通过在“a”侧或“b”侧上给它们顶点来启动和关闭节点。 我们可以通过调整成本来模拟通信速度的差异。 我们可以通过限制他们的弧的容量来限制两个节点交换物品的数量。 我们可以引入更多内部顶点并改变完全二分连接的拓扑结构,以模拟网络争用的影响。

3
我不相信你所描述的问题足够复杂,以至于吸引了独立的研究。如果 machine count(称之为 C)在数千台以上,并且你的样本计数甚至达到了万亿级别,那么将样本计数发送到协调主节点是微不足道的。
主节点可以轻易地(O(C))计算 N,确定哪些节点违反了此限制,以及超出多少。请注意,超出限制的总和恰好是所需通信的最小量。通过在计算N时插入一个松弛参数(即接受负载不均),您可以控制这个数量。
使用排序,可以在 O(ClogC) 的时间内获取按样本计数排序的节点顺序。沿着两端的指针向中间移动,每次在大节点和小节点之间进行传输,大小为大节点中剩余过量或小节点中剩余松弛度的最小值。让上一个句子中具有活动约束的节点指针前进并重复操作,直到新的大节点没有过量即可(我认为这就是 @Noxville 所说的贪婪算法)。
假设 N 大于每个节点的平均样本计数,则很容易看出这是理想的,从而实现了最小通信。
如果你的机器网络有任何约束,比如缺少链接或边上的最大流量或变量成本,则需要使用图形流程。但是您没有提及这方面的任何内容,而同一数据中心内的计算机集群通常没有这些限制。

1

谢谢你的回答。我不打算添加或删除计算节点,那我还需要使用它吗?我并没有看到它如何解决我提出的问题。 - Gus
良好一致性哈希只是分布式哈希表的一种专门形式。如果您从技术上不添加或删除节点,则可以执行稍微简单的操作。通常,人们只需对数据的ID进行哈希,然后通过节点数取模以确定数据所属的节点。 - nickmbailey
我想要最小化通信。如果一个节点有多余的物品,而另一个节点有足够的空位,我想将所有多余的物品从一个节点移动到另一个节点。只要某个节点包含某些物品,我就不需要知道哪个节点包含它们。 - Gus
像这样的哈希解决方案可以确保在所有节点上均匀分布。假设您使用了一个良好分布的哈希函数(md5 可以很好地工作),并且不仅仅是几个项目。 - nickmbailey
我认为你没有考虑到我想要将节点之间的通信最小化。如果节点A超出50个项目的容量,而节点B、C和D有100个空闲插槽,我不想在B、C和D之间均匀分配。我想选择B、C或D中的一个,并将所有50个项目移动到该节点。 - Gus
显示剩余2条评论

1

我认为问题应该这样澄清:

M 个剩余节点和 K 个赤字节点,我们应该在节点之间重新分配样本,使得剩余节点数量为0,而(可能)一些赤字节点。样本应该以包的形式交换,并且这些包的数量应该最小化。

或者,从数学上讲,我们有一个 M*K 矩阵,其中每个单元格表示要从节点 M 传递到节点 K 的样本数量,每行的元素总和和每列的元素总和都有给定的最大值。目标是最小化非零单元格的数量。

这是一种 "约束满足问题"。它是 NP 完全的。我发现两个经典问题与此问题类似:“集合打包”和“广义精确覆盖”。

为了将问题简化为"集合覆盖", 我们需要添加(暂时)几个多余节点,每个节点有N+1个样本,以便在重新分配后没有剩余节点。然后对于每个节点,我们需要为多余元素和“赤字”元素生成所有可能的分区。然后对于多余和赤字分区的笛卡尔积应用“集合覆盖”的“优化”版本,找到最小子集的数量。
为了将问题简化为"广义精确覆盖", 对于每个节点,我们需要为多余元素和“赤字”元素生成所有可能的分区。然后我们应该添加MM+1等优化节点来最小化覆盖中的子集数量。然后对于多余和赤字分区以及优化节点的笛卡尔积应用“广义精确覆盖”。对于较少数量的优化节点,此算法将失败,对于一些更大的数量,它将找到最小子集的数量。
“广义精确覆盖问题”可以使用{{link1:“Knuth的X算法”}}进行求解。我不知道任何针对“集装箱装载问题”的算法。
所有这些解决方案都能给出精确答案,但它们的计算复杂度极高,很少有人在实际的调度程序中使用它们。实用的方法是使用一些启发式和贪心算法。只需对剩余节点和赤字节点进行排序,并应用“最佳拟合”策略即可。

1

通常我们称之为数据重分配,理解是如果你正在重新分配它,你希望在某些度量标准下(如任务之间的均匀性)分配最优。

当你尝试进行计算负载平衡时,这在科学/技术计算中确实会出现。即使你在多个维度上进行计算,如果你正在通过空间填充曲线将空间数据分配给处理器,那么这个问题就会出现,而你通常希望数据被均匀分配。

该过程非常简单;首先,您需要对xi进行独占性前缀和,以便知道在你左边有多少项。例如,对于上面Noxville的示例数据:

[9, 6,  1,  6,  2] 

前缀和应为:

[0, 9, 15, 16, 22]

你会发现(从最后一个处理器的总和加上它有多少个)总共有24个项目。

然后,你要计算出你理想的分区大小——比如说,ceil(totitems / nprocs)。你可以用任何方法来计算,只要每个处理器都同意所有分区大小即可。

现在,你有几种方法可以继续进行。如果数据项在某种意义上很大,并且你不能在内存中有两个副本,那么你可以开始将数据移动到最近的邻居处。你知道你左边的项目数量以及该方向上的“过剩”或“不足”;你还知道你有多少个项目(并且在你完成修复过剩或不足之后,你也会知道你将拥有多少个项目)。因此,你开始向左右邻居发送数据,并从左右邻居接收数据,直到向左的处理器集体拥有正确数量的项目,你也是如此。

但是,如果您能够负担得起两份数据的副本,那么您可以采取另一种方法来最小化发送的消息数量。您可以将左侧单元格的数量视为您的本地数据进入“全局”数组的起始索引。由于您知道每个处理器最终会有多少项,因此可以直接确定这些项将要到达哪个进程,并将它们直接发送。(例如,在上面的示例中,具有0..8项的处理器0知道,如果除了最后一个处理器之外的每个处理器都将以5个数据项结束,则值5-8可以发送到处理器1。)一旦发送了这些数据,您只需接收所期望的数据量即可完成。

以下是在C和MPI中执行此操作的简单示例,但基本方法几乎可以在任何地方使用。MPI的前缀扫描操作生成包含总和,因此我们必须减去自己的值以获得独占总和:

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <time.h>

void initdata(const int rank, const int maxvals, char **data, int *nvals) {
    time_t t;
    unsigned seed;

    t = time(NULL);
    seed = (unsigned)(t * (rank + 1));

    srand(seed);
    *nvals = (rand() % (maxvals-1)) + 1;
    *data = malloc((*nvals+1) * sizeof(char));

    for (int i=0; i<*nvals; i++) {
        (*data)[i] = 'A' + (rank % 26);
    }
    (*data)[*nvals] = '\0';
}

int assignrank(const int globalid, const int totvals, const int size) {
    int nvalsperrank = (totvals + size - 1)/size;
    return (globalid/nvalsperrank);
}

void redistribute(char **data, const int totvals, const int curvals, const int globalstart,
                  const int rank, const int size, int *newnvals) {

    const int stag = 1;
    int nvalsperrank = (totvals + size - 1)/size;

    *newnvals = nvalsperrank;
    if (rank == size-1) *newnvals = totvals - (size-1)*nvalsperrank;

    char *newdata = malloc((*newnvals+1) * sizeof(char));
    newdata[(*newnvals)] = '\0';

    MPI_Request requests[curvals];

    int nmsgs=0;

    /* figure out whose data we have, redistribute it */
    int start=0;
    int newrank = assignrank(globalstart, totvals, size);
    for (int val=1; val<curvals; val++) {
        int nextrank = assignrank(globalstart+val, totvals, size);
        if (nextrank != newrank) {
            MPI_Isend(&((*data)[start]), (val-1)-start+1, MPI_CHAR, newrank, stag, MPI_COMM_WORLD, &(requests[nmsgs]));
            nmsgs++;
            start = val;
            newrank = nextrank;
        }
    }
    MPI_Isend(&((*data)[start]), curvals-start, MPI_CHAR, newrank, stag, MPI_COMM_WORLD, &(requests[nmsgs]));
    nmsgs++;

    /* now receive all of our data */
    int newvalssofar= 0;
    int count;
    MPI_Status status;
    while (newvalssofar != *newnvals) {
        MPI_Recv(&(newdata[newvalssofar]), *newnvals - newvalssofar, MPI_CHAR, MPI_ANY_SOURCE, stag, MPI_COMM_WORLD, &status);
        MPI_Get_count(&status, MPI_CHAR, &count);
        newvalssofar += count;
    }

    /* wait until all of our sends have been received */
    MPI_Status statuses[curvals];
    MPI_Waitall(nmsgs, requests, statuses);

    /* now we can get rid of data and relace it with newdata */
    free(*data);
    *data = newdata;
}

int main(int argc, char **argv) {
    const int maxvals=30;
    int size, rank;
    char *data;
    int mycurnvals, mylvals, myfinalnvals;
    int totvals;

    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    initdata(rank, maxvals, &data, &mycurnvals);

    MPI_Scan( &mycurnvals, &mylvals, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD );
    if (rank == size-1) totvals = mylvals;
    mylvals -= mycurnvals;

    MPI_Bcast( &totvals, 1, MPI_INT, size-1, MPI_COMM_WORLD );

    printf("%3d      : %s %d\n", rank, data, mylvals);

    redistribute(&data, totvals, mycurnvals, mylvals, rank, size, &myfinalnvals);


    printf("%3d after: %s\n", rank, data);

    free(data);
    MPI_Finalize();
    return 0;
}

运行此代码,您将获得预期的行为;请注意,我确定“期望”分区的方式(使用ceil(totvals / nprocesses)),最终处理器通常会负载不足。此外,我没有尝试确保在重新分配中保留顺序(尽管如果顺序很重要,则很容易做到):

$ mpirun -np 13 ./distribute 
  0      : AAAAAAAAAAA 0
  1      : BBBBBBBBBBBB 11
  2      : CCCCCCCCCCCCCCCCCCCCCCCCCC 23
  3      : DDDDDDD 49
  4      : EEEEEEEEE 56
  5      : FFFFFFFFFFFFFFFFFF 65
  6      : G 83
  7      : HHHHHHH 84
  8      : IIIIIIIIIIIIIIIIIIIII 91
  9      : JJJJJJJJJJJJJJJJJJ 112
 10      : KKKKKKKKKKKKKKKKKKKK 130
 11      : LLLLLLLLLLLLLLLLLLLLLLLLLLLL 150
 12      : MMMMMMMMMMMMMMMMMM 178

  0 after: AAAAAAAAAAABBBBB
  1 after: BBBBBBBCCCCCCCCC
  2 after: CCCCCCCCCCCCCCCC
  3 after: DDDDDDDCEEEEEEEE
  4 after: EFFFFFFFFFFFFFFF
  5 after: FFFHHHHHHHIIIIIG
  6 after: IIIIIIIIIIIIIIII
  7 after: JJJJJJJJJJJJJJJJ
  8 after: JJKKKKKKKKKKKKKK
  9 after: LLLLLLLLLLKKKKKK
 10 after: LLLLLLLLLLLLLLLL
 11 after: LLMMMMMMMMMMMMMM
 12 after: MMMM

0

我认为这个重新分配问题与计算机中的负载平衡有所不同。

负载平衡算法术语通常意味着一组策略/启发式,以确保未来负载(而不是当前负载)相对均匀分布。

在这种情况下,负载均衡器不会重新分配现有服务器/系统的负载,而是任何新请求到来时,它将尝试使用某些策略(即最小负载、轮询等)进行分配,从而在长期内使服务器相对均衡负载。

http://en.wikipedia.org/wiki/Load_balancing_(computing)

这个问题中的负载重新分配,可以通过迭代地将物品从最大负载的箱子移动到最小负载的箱子来实现。


我不太确定是否应该称其为负载均衡,但我认为这个概念是相关的,或者至少这是我现在脑海中唯一的查询词。您区分了“未来”负载和“当前”负载。重新采样后,一个服务器可能会有另一个服务器的两倍粒子数。对这些粒子进行的计算可以被视为“未来”负载,您可以通过分配当前样本集来平衡它。 - Gus

0

基本上你想从

[9, 6, 1, 6, 2] 

[5, 5, 4, 5, 5]

我认为最好的方法是计算floor(sum(array)/len(array)),然后评估到达此位置所需的差分。在这种情况下,floor((9+6+1+6+2)/5)=4,因此我们正在查看一个初始差分[-5,-2,+3,-2,+2]。然后,您可以贪心地交换相邻对中符号不同的值(例如将array[2]中的2转移到arr[1]和将array[4]中的2转移到array[3])。然后,您将剩下[-5,0,1,0,0],从这里您可以贪心地分配剩余的位。


我真的在寻找一个名称。你提出的算法很直观,但我想要一些能够最小化或接近最小化交换的东西。 - Gus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接