09 Distributed Consensus & Task Allocation · #40 of 46

Auction Algorithm

How robots divide the work by bidding against each other

A warehouse automated guided vehicle (a low forklift robot) carrying a load down an aisle.
A warehouse AGV: one robot, one job at a time, in a fleet with no foreman. Deciding which vehicle fetches which pallet is the task-allocation problem, and an auction lets the fleet settle it by bidding instead of waiting on a planner. · AGVExpertJS, CC BY-SA 3.0

Ten robots, ten tasks, no boss. Nobody is allowed to say "you go there, you go here." And yet, in a few noisy rounds of haggling, every robot ends up on exactly the right job, within a hair of the mathematically perfect split.

The trick is older than robotics. It's an auction.

The hard problem hiding here is called task allocation: given a fleet of robots and a list of jobs, who does what? The obvious answer, a central planner that crunches every pairing and barks out orders, works beautifully right up until the planner's radio drops a packet, or the fleet grows to a thousand robots, or the one machine running the optimizer falls over. Then your whole swarm is staring at a dead leader, waiting.

So we throw out the leader. The robots will figure it out themselves, by bidding.

The auction analogy

Picture an estate sale. Each item on the block is a task. Each bidder is a robot. Every robot privately knows how much a given task is worth to it: a task right next door is worth more than one across the warehouse. The current asking price of each task is public.

A robot's bid is dead simple: it wants the task with the best value minus price.

bij=valueijpricejb_{ij} = \text{value}_{ij} - \text{price}_j

The highest bidder takes the task. Then the price of that task ticks up. Repeat. That's the entire algorithm:

  1. Each unassigned robot bids on the task with the best value-minus-price.
  2. The highest bidder wins it, bumping whoever held it back into the pool.
  3. The won task's price rises by a small step: pricejpricej+ϵ\text{price}_j \leftarrow \text{price}_j + \epsilon.
  4. Repeat until nobody wants to switch.

The genius is in step 3. The rising price is the only coordination signal there is. No robot ever tells another robot what to do. A popular task simply gets expensive until it's no longer the best deal for everyone but one, and the losers, priced out, drift toward jobs nobody else wanted. The market does the planning.

Watch it settle (in your head)

There's no live canvas on this page, so run the auction on your mental whiteboard. Three robots, three tasks.

The bid, with the costs spelled out

In a real fleet the "value" of a task is mostly reward minus the cost of getting there. So the bid grows a couple of terms:

bij=rijcdijpjb_{ij} = r_{ij} - c \cdot d_{ij} - p_j

Read it plainly: a robot wants a high-reward task that's close and not yet expensive. The price term pjp_j is the one the other robots get to push on. Everything else is private.

The auction was not the first solution to this problem, and it isn't the only one. There's an older, centralized method that gets the exact same job done.

Why does raising prices guarantee a near-optimal answer?

The auction is solving the assignment problem (pair nn robots to nn tasks to maximize total reward), and it does so by keeping a running set of prices pjp_j and bidding only on tasks that satisfy approximate complementary slackness. A robot ii is "happy" with task jj if that task is within ϵ\epsilon of its best available net value:

rijpj    maxk(rikpk)ϵr_{ij} - p_j \;\ge\; \max_{k}\,\bigl(r_{ik} - p_k\bigr) - \epsilon

When every robot is happy under this rule, the assignment is locked, and Bertsekas proved its total reward is within nϵn\epsilon of the true optimum:

R\*Rauction    nϵR^\* - R_{\text{auction}} \;\le\; n\,\epsilon

That's where "ϵ\epsilon-optimal" comes from. Each bid raises the winning task's price by at least ϵ\epsilon, so prices are strictly increasing and the process must terminate: no infinite haggling. Shrink ϵ\epsilon and the bound tightens toward exact optimality, at the cost of more rounds. The single parameter trades optimality against speed, and the proof hands you the exact exchange rate.

A reference step

The core loop is almost embarrassingly short. Every robot scans the tasks, picks its best net bid, and nudges the price if it switched:

fn auction_step(robots: &[Agent], tasks: &mut [Task], epsilon: f32) -> bool {
    let mut changed = false;

    for i in 0..robots.len() {
        let mut best_j = None;
        let mut best_bid = f32::NEG_INFINITY;

        for j in 0..tasks.len() {
            let dist = robots[i].pos.distance(tasks[j].pos);
            let bid = tasks[j].reward - dist - tasks[j].price;
            if bid > best_bid {
                best_bid = bid;
                best_j = Some(j);
            }
        }

        if let Some(j) = best_j {
            if robots[i].assigned_task != Some(j) {
                changed = true;
                robots[i].assigned_task = Some(j);
                tasks[j].price += epsilon;
            }
        }
    }

    changed
}

Call it until it returns false (that's the "nobody wants to switch" condition) and the fleet has divided its own labor. To stress-test it, make the messages arrive late: have robots act on stale prices and watch convergence merely slow, not fail.

Key takeaways

A swarm with no leader still manages to share the work, not because some clever rule was etched into every robot, but because each one was simply allowed to want the best deal it could get. Raise the price of what's popular, and selfish little agents arrange themselves into something that looks, from above, like a plan.

It's the oldest coordination trick we have. The robots just finally learned to bid.

full glossary →