Skip to content
academia.sh

Lesson 11 / 11

Cost and Cluster Sizing

Counting that the decision of how many workers a cluster is set up with is made between makespan and total work, and that the two numbers go in opposite directions: the same job of 2200 rows gives a makespan of 2250, 3691, 3179, 2610 and 1398 and a total work of 2200, 5830, 7414, 8074 and 8635 on one, two, four, eight and sixteen workers; as the makespan falls the total work rises, and the sizing decision cannot be made without writing which one is paid. The second worker raising the makespan from 2250 to 3691 comes from the shuffle coming into being, and even with a good key of skew 1.00 a rise from 2250 to 2800 is seen. The product of workers and makespan climbs one way from 2250 to 22368, when the fixed cost is counted per worker the gain of sixteen workers falls from 852 to 102, and when the network cost rises to 12 the setup with the shortest makespan returns to a single worker. The lesson closes the course and gathers the numbers of eleven lessons in one table.

Published Updated

In this lesson

  • Sweeps the same job between one and sixteen workers and counts that the makespan and the total work go in separate directions.
  • Makes the cluster sizing decision separately by the measures of makespan, total work and the product of workers and makespan.
  • Shows the sensitivity of the decision to the network cost, the way the fixed cost is counted and the choice of partition key.
Contents

The previous lesson wrote the fifth name of the cost: the recomputed row. Before it had come the shuffled row, the copy, the skew and the held row. Beneath the five names there stood always the same two numbers: how long a job took and how much work it did in total. What remains is the question the course is asked most often: with how many workers should this cluster be set up.

The question looks innocent and has no answer, because it is incomplete. Growing a cluster changes two numbers at the same time, and they go in opposite directions. This lesson sweeps the number of workers from one to sixteen, writes both at every step, and counts which measure, which choice and which partition key the decision hangs on.

  • PF53. The source, the pipeline and the distributed setup are fictional and are generated within the lesson. No distributed processing framework, cluster manager or cloud service is called. The seed is 20260218.
  • PF54. The procedure of cost accounting was measured in the Cloud Governance, Security and Cost course of the Cloud Computing curriculum and in the Building Applications with Model APIs course of the AI Engineering curriculum; it is not repeated. Here the cost is written only as makespan and total work; no currency or price is used.
  • PF55. Makespan is the work of the busiest worker and is the model’s own unit, not raw milliseconds. Total work is the sum of the work done by all the workers.
  • PF56. NETWORK_COST = 3 and TASK_OVERHEAD = 50 are choices; the result of this lesson is sensitive to both, and below both are handled together with their values.
  • PF57. cost counts the fixed cost once per job; the Resource Managers lesson counts the same cost per worker. The two ways of counting are written here side by side as two columns.
  • PF58. The baseline is the single worker: no shuffle, skew 1.00, makespan 2250, total work 2200.

The Worker Sweep

The first block sets up the source, the three stages of the pipeline up to consumption, and the measures of partitioning and cost. The job measured is the job used throughout the course: the rows are partitioned by the region key and then shuffled to the subscriber key.

# FICTIONAL. Meter readings of the measurement network, the first three stages of the pipeline and
# the distributed setup are generated within the lesson; no distributed processing framework,
# cluster manager or cloud service is called. Seed 20260218.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


REGION = ["dogu", "bati", "kuzey", "guney", "merkez"]
TYPE = ["residential", "commercial"]
PERIODS = [f"2026-{a:02d}" for a in range(1, 13)]
SUBSCRIBERS = {}
for i in range(200):
    r = rng(SEED + 37 * i)
    SUBSCRIBERS[f"A{i + 1:03d}"] = {"region": REGION[int(r() * 5)], "type": TYPE[int(r() * 2)],
                                    "start": 10000 + int(r() * 60000)}


def generate_source():
    records = []
    for i, (sub, a) in enumerate(SUBSCRIBERS.items()):
        e = a["start"]
        for p in PERIODS:
            r = rng(SEED + 101 * i + 7 * PERIODS.index(p))
            e += 4 + int(r() * 46)
            records.append({"subscriber": sub, "period": p, "region": a["region"], "type": a["type"],
                            "index": e, "unit": "m3"})
    return records


def s1_screen(records):
    REQUIRED = ("subscriber", "period", "region", "index", "unit")
    keep = [k for k in records if all(a in k for a in REQUIRED)]
    return keep, len(records) - len(keep)


def s2_dedupe(records):
    seen, keep = set(), []
    for k in records:
        key = (k["subscriber"], k["period"])
        if key in seen:
            continue
        seen.add(key)
        keep.append(k)
    return keep, len(records) - len(keep)


def s3_consumption(records):
    table = {(k["subscriber"], k["period"]): k for k in records}
    out, dropped = [], 0
    for k in records:
        i = PERIODS.index(k["period"])
        if i == 0:
            continue
        prev = table.get((k["subscriber"], PERIODS[i - 1]))
        if prev is None:
            dropped += 1
            continue
        out.append(dict(k, m3=int(k["index"]) - int(prev["index"])))
    return out, dropped


WORKERS, NETWORK_COST, TASK_OVERHEAD = 4, 3, 50


def rows():
    v, _ = s1_screen(generate_source())
    v, _ = s2_dedupe(v)
    v, _ = s3_consumption(v)
    return v


def trace_id(m):
    h = 2166136261
    for c in str(m):
        h = ((h ^ ord(c)) * 16777619) & 0xFFFFFFFF
    return h


def distribute(records, key, workers=WORKERS):
    buckets = {i: [] for i in range(workers)}
    for k in records:
        buckets[trace_id(k[key]) % workers].append(k)
    return buckets


def load(buckets):
    n = [len(v) for v in buckets.values()]
    return {"total": sum(n), "busiest": max(n), "least": min(n),
            "skew": round(max(n) / (sum(n) / len(n)), 4)}


def shuffle(buckets, new_key, workers=WORKERS):
    new = {i: [] for i in range(workers)}
    moved = 0
    for old, records in buckets.items():
        for k in records:
            target = trace_id(k[new_key]) % workers
            if target != old:
                moved += 1
            new[target].append(k)
    return new, moved


def cost(buckets, moved=0, workers=WORKERS, network=NETWORK_COST, overhead=TASK_OVERHEAD):
    """Makespan = the work of the busiest worker (parallel). Total work is counted separately."""
    n = [len(v) for v in buckets.values()]
    return {"duration": max(n) + moved * network // workers + overhead,
            "total_work": sum(n) + moved * network, "moved": moved}


ALL = rows()
COUNTS = (1, 2, 4, 8, 16)
print(f"job {len(ALL)} rows , partition key region , shuffle to subscriber")
print("workers  skew  moved  makespan  total work  workers x makespan  makespan (overhead per worker)")
for i in COUNTS:
    bk = distribute(ALL, "region", i)
    _, t = shuffle(bk, "subscriber", i)
    m = cost(bk, t, i)
    mp = cost(bk, t, i, overhead=i * TASK_OVERHEAD)
    print(f"{i:>7}  {load(bk)['skew']:>4}  {t:>5}  {m['duration']:>8}  {m['total_work']:>10}"
          f"  {i * m['duration']:>18}  {mp['duration']:>30}")
job 2200 rows , partition key region , shuffle to subscriber
workers  skew  moved  makespan  total work  workers x makespan  makespan (overhead per worker)
      1   1.0      0      2250        2200                2250                            2250
      2  1.66   1210      3691        5830                7382                            3741
      4  3.32   1738      3179        7414               12716                            3329
      8  6.64   1958      2610        8074               20880                            2960
     16  6.88   2145      1398        8635               22368                            2148

The makespan column does not fall in a straight line: it rises from 2250 to 3691, then falls to 3179, 2610 and 1398. It drops below the single worker only at sixteen workers. The total work column, on the other hand, never turns back: from 2200 to 5830, to 7414, to 8074 and to 8635 it climbs one way. The same set of rows, the same result, four times the total work.

The second row is the clearest example of the second claim of the course. With a single worker there is no shuffle; the moment the second worker is added, 1210 rows have to cross the network, the total work jumps from 2200 to 5830, and because the share of the busiest worker is still large, the makespan also rises. Parallelism is not free, and its first step is negative.

The last two columns show how the decision turns. The product of workers and makespan, which is the equivalent of renting the cluster for the length of the job, climbs from 2250 to 22368 without ever falling; by this measure the best setup is the single worker. When the fixed cost is counted per worker, the makespan of sixteen workers rises from 1398 to 2148, and its advantage over the single worker falls from 852 units to 102 units. A choice as small as TASK_OVERHEAD = 50 nearly erases the gain of a setup sixteen times as large.

What the Decision Hangs On

The three measures gave three separate answers. The next question is how much these answers depend on the two choices in the model and on the partition key. The second block sweeps the network cost, and then repeats the same sweep with a key that has no skew.

# This block continues the definitions of ALL, COUNTS, distribute, load, shuffle and cost from the first block.
print("network cost  " + "  ".join(f"{i:>5}" for i in COUNTS) + "   shortest makespan")
for network in (1, 3, 6, 12):
    s = []
    for i in COUNTS:
        bk = distribute(ALL, "region", i)
        _, t = shuffle(bk, "subscriber", i)
        s.append(cost(bk, t, i, network=network)["duration"])
    print(f"{network:>12}  " + "  ".join(f"{v:>5}" for v in s) + f"   {COUNTS[s.index(min(s))]:>17}")

print("\npartition key subscriber , shuffle to period")
print("workers  skew  moved  makespan  total work")
for i in COUNTS:
    bk = distribute(ALL, "subscriber", i)
    _, t = shuffle(bk, "period", i)
    m = cost(bk, t, i)
    print(f"{i:>7}  {load(bk)['skew']:>4}  {t:>5}  {m['duration']:>8}  {m['total_work']:>10}")
network cost      1      2      4      8     16   shortest makespan
           1   2250   2481   2310   2120   1130                  16
           3   2250   3691   3179   2610   1398                  16
           6   2250   5506   4483   3344   1800                  16
          12   2250   9136   7090   4813   2604                   1

partition key subscriber , shuffle to period
workers  skew  moved  makespan  total work
      1   1.0      0      2250        2200
      2   1.0   1100      2800        5500
      4  1.02   1651      1849        7153
      8  1.04   1927      1058        7981
     16   1.2   2066       602        8398

The first table shows what it means that NETWORK_COST is a choice. At a cost of 1, 3 and 6 sixteen workers give the shortest makespan; at a cost of 12 the winner returns to the single worker, because the number 2604 is above 2250. Once the ratio of the network to local processing changes, the case for a cluster sixteen times as large disappears. The sizing decision is not a decision of the measured job but of the ratio between the network and the computation, and without that ratio written the result cannot be carried.

The second table separates out the share of skew. With the subscriber key the skew does not exceed 1.20 up to sixteen workers, and the makespan falls to 602, far below the 1398 of the region key. On the other hand the second worker raising the makespan happens here too: from 2250 to 2800. The shuffle comes into being even with a good key, and at the first step it is larger than its gain. The second claim of the course is not a consequence of skew; skew only deepens it.

Summary

  • The same job of 2200 rows gives a makespan of 2250, 3691, 3179, 2610, 1398 and a total work of 2200, 5830, 7414, 8074, 8635 on one, two, four, eight and sixteen workers; as the makespan falls the total work rises.
  • The second worker raises the makespan from 2250 to 3691: the shuffle comes into being, 1210 rows cross the network and the total work jumps to 5830. The makespan falls below the single worker only at sixteen workers.
  • Three measures choose three separate setups: makespan chooses sixteen workers, total work the single worker, and the product of workers and makespan (from 2250 to 22368) again the single worker. The decision cannot be made without writing which measure is paid.
  • When TASK_OVERHEAD = 50 is counted per worker, the makespan of sixteen workers becomes 2148 and the gain falls from 852 to 102; when NETWORK_COST rises to 12, the setup with the shortest makespan returns to the single worker.
  • With the subscriber key, which has no skew, the makespan falls as far as 602, but the second worker raising the makespan from 2250 to 2800 happens there too; the cost of the shuffle at the first step is independent of skew.

Course Wrap-Up

The rows carry no common unit; by which measure each row is read stands next to the lesson name. The difference in parentheses is the distributed setup minus the single-worker baseline; the direction of the sign is read from the measure name.

Lesson Single-worker baseline Distributed setup Cost
Cluster Computing Fundamentals — makespan and total work single worker makespan 2250, total work 2200 four workers with subscriber makespan 611 (−1639), total work 2200 (0) total work is 2200 in all four setups and does not tell them apart
Distributed File Systems — locally read blocks and makespan single copy 5/22 local, makespan 1850 replication 3 gives 22/22 (+17), makespan 650 (−1200) the stored blocks rise from 22 to 66, and a fourth copy adds nothing to locality
MapReduce Model — shuffled rows and total work map shuffled 0, total work 2200 direct reduction shuffled 1738 (+1738), total work 7414 (+5214) pre-reduction gives the same result with 20 intermediate rows and a total work of 2245
Distributed Datasets — rows read and partitions opened eager plan 6556 rows, 44 partitions lazy plan 200 rows (−6356), 1 partition (−43) a type error in the plan shows up only on the first action, after 200 rows have been read
Resource Managers — makespan and fixed cost paid single slot makespan 3000, fixed cost 800 sixteen slots makespan 215 (−2785), fixed cost 800 (0) when every task is given a slot, the gain from eight to sixteen stays at 121 and is beaten by the added 400
Shuffle Cost — shuffled rows and makespan shuffled 0 in the subscriber layout, makespan 611 shuffling to region moves 1738 (+1738), makespan 1914 (+1303) the total work rises from 2200 to 7414; 5214 units are pure movement that does no computation
Joins at Scale — join makespan, size of 4000 rows broadcast 5876 partitioned 5429 (−447) the crossover point is 2980, and at network costs 6 and 12 partitioned cannot get ahead at any size
Data Skew — skew and makespan single worker skew 1.00, makespan 2250 two workers with region skew 1.66 (+0.66), makespan 3691 (+1441) at sixteen workers skew 6.88, 13 idle workers; the makespan cannot fall below 589 at any number
Caching and Persistence — total of two queries uncached 5500 cached 3300 (−2200) if the cache is not held the total is again 5500, and with a single query the gain is 0
State in Stream Engines — recomputed rows no checkpoint 1600 interval 3 gives 400 (−1200) the certain cost rises from 0 to 800, yet interval 6 gives the same 400
Cost and Cluster Sizing — makespan and total work single worker makespan 2250, total work 2200 sixteen workers makespan 1398 (−852), total work 8635 (+6435) the second worker raises the makespan to 3691; when the fixed cost is counted per worker the gain falls to 102

The table is a single rule written eleven times. The number of a distributed job is not one number: the makespan and the total work are measured separately, and a claim of speedup that does not say which was measured is incomplete. The places where the two columns go in opposite directions in the same row are the rule itself: in MapReduce Model the makespan falls while the total work rises from 2200 to 7414, and in this lesson the makespan falls by 852 units while the total work climbs by 6435 units. Cluster Computing Fundamentals shows its mirror: the makespan of the four setups varies between 611 and 2250 while the total work is 2200 in all four, so a report that writes only the total work cannot tell any of these setups apart.

The second claim is read from the same table: adding workers makes the job worse. In Data Skew the second worker raises the makespan from 2250 to 3691 and carries the total work to 5830; in this lesson the same step raises the makespan to 2800 even with the subscriber key, whose skew is 1.00. In Resource Managers the 121 units gained in going from eight to sixteen tasks are beaten by the added 400 units of fixed cost. In Distributed File Systems a fourth copy raises the stored block to 88 and adds nothing to locality. In Caching and Persistence, in a job with a single query the cache holds 2200 rows and gains 0, and in State in Stream Engines lowering the interval from six to three raises the certain cost from 400 to 800 and changes the recovery not at all. In six lessons the added resource was paid for and nothing was received in return.

The third claim is quieter: the result is the same in every setup. In MapReduce Model the five region totals presented by the three setups are exactly the same, although one spends 7414 and another 2245 of total work. In Shuffle Cost the output does not change while the makespan rises from 611 to 1914, in Joins at Scale the two join strategies match the same rows at every size, in Data Skew the three partitionings produce the same region totals, in Caching and Persistence a cache that is not held changes not the result but only the cost, and in State in Stream Engines all four intervals give the same result as long as exactly-once is achieved. No check that tests correctness shows a wrong partitioning; a wrong partitioning becomes visible only when it is counted, and as long as it is not counted the bill silently grows.

In eleven lessons the data was split, moved, balanced, stored and recovered; in twenty-seven lessons it was ingested, transformed, stored and queued. Where this data goes was never asked. The very model it feeds, whether the same model can be reproduced with the same data, and how a trained model goes to production were not measured in any lesson. The next course takes the output of the pipeline as input and counts the reproducibility, the versioning and the deployment of a training run.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close