Skip to content
VLSI Mentor

Python Part II · Module 27 · Parallel Regression

Why Parallelism, and What It Cannot Fix

Module 26 built a regression that runs four hundred jobs correctly, one after another, and a regression that takes five days is a regression nobody runs. Running the jobs at the same time is the obvious answer and it is the right one — the jobs were made independent four chapters ago precisely so that it would be. What is worth establishing before writing any of it is which problems concurrency actually solves, because the famous theoretical limit turns out not to apply here and the two real limits are not about speed at all.

Advanced18 min readPythonPart IIAutomationRegressionVerification Flow

Part II · Module 27 · Chapter 27.1 · Why Parallelism, and What It Cannot Fix

1. A Regression Nobody Runs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOBS, MINUTES_EACH = 600, 12

serial_minutes = JOBS * MINUTES_EACH

print("jobs            :", JOBS)
print("minutes each    :", MINUTES_EACH)
print("serial total    :", serial_minutes, "minutes")
print("                 =", f"{serial_minutes / 60:.0f} hours")
print("                 =", f"{serial_minutes / 1440:.1f} days")
print()
print("runs per week   :", f"{7 / (serial_minutes / 1440):.1f}")
print("feedback delay  :", "a change made on Monday is tested by Saturday")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
jobs            : 600
minutes each    : 12
serial total    : 7200 minutes
                 = 120 hours
                 = 5.0 days

runs per week   : 1.4
feedback delay  : a change made on Monday is tested by Saturday

1.4 runs per week. Everything else in this module follows from that number: a regression slower than the rate at which the design changes is not a safety net, it is an archive.

Every figure in this chapter is arithmetic over configured inputs — a job count, an assumed per-job duration, a worker count. Nothing here is measured, and Chapter 27.2 is specifically about why you must measure your own rather than trust a number like 12.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 600 jobs x 12 min serial = 5 days
# 32 at a time        = ~4 hours
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOBS, MINUTES_EACH = 600, 12
serial = JOBS * MINUTES_EACH

print(f"{'workers':>8} {'minutes':>9} {'hours':>7} {'speedup':>8}")
for workers in (1, 8, 16, 32, 64):
    parallel = serial / workers
    print(f"{workers:>8} {parallel:>9.0f} {parallel / 60:>7.2f} {serial / parallel:>7.0f}x")

print()
print("the annotation's claim:", "32 at a time is about 4 hours")
print("the arithmetic says   :", f"{serial / 32 / 60:.2f} hours")
print("'~4 hours' is honest  :", 3.5 <= serial / 32 / 60 <= 4.5)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 workers   minutes   hours  speedup
       1      7200  120.00       1x
       8       900   15.00       8x
      16       450    7.50      16x
      32       225    3.75      32x
      64       112    1.88      64x

the annotation's claim: 32 at a time is about 4 hours
the arithmetic says   : 3.75 hours
'~4 hours' is honest  : True

Five days to under four hours, and the speedup column is perfectly linear, because the model assumes the jobs are independent and nothing is shared.

That assumption is doing an enormous amount of work, and it is the one Module 26 spent nine chapters earning. Chapter 26.4 gave every job its own directory, Chapter 26.2 fixed the job list up front, Chapter 26.6 reconciled against it rather than against a process. The jobs are already independent, which is why this module is five chapters rather than twenty.

3. The Famous Limit

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def amdahl(serial_fraction, workers):
    return 1 / (serial_fraction + (1 - serial_fraction) / workers)


print(f"{'serial part':>12} {'8':>8} {'32':>8} {'128':>8} {'ceiling':>9}")
for s in (0.00, 0.01, 0.05, 0.20):
    row = " ".join(f"{amdahl(s, n):>8.1f}" for n in (8, 32, 128))
    ceiling = "unbounded" if s == 0 else f"{1 / s:.0f}x"
    print(f"{s:>12.0%} {row} {ceiling:>9}")

print()
print("at 5% serial, 128 workers buy:", f"{amdahl(0.05, 128):.1f}x")
print("and the ceiling is           :", f"{1 / 0.05:.0f}x")
print("so the last 96 workers buy   :", f"{amdahl(0.05, 128) - amdahl(0.05, 32):.1f}x more")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 serial part        8       32      128   ceiling
          0%      8.0     32.0    128.0 unbounded
          1%      7.5     24.4     56.4      100x
          5%      5.9     12.5     17.4       20x
         20%      3.3      4.4      4.8        5x

at 5% serial, 128 workers buy: 17.4x
and the ceiling is           : 20x
so the last 96 workers buy   : 4.9x more

Amdahl's law: whatever fraction of the work cannot be parallelised sets a ceiling no number of workers can exceed. At twenty percent serial, a hundred and twenty-eight workers buy 4.8×.

It is the right tool for asking the question. It is also, for a verification regression, usually the wrong answer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PARTS = [("expand the job list", 1, "Chapter 26.2, once"),
         ("compile the design", 1, "Chapter 25.3 §7, shared by every job"),
         ("run the jobs", 600, "independent"),
         ("aggregate and report", 1, "Chapter 26.6, once")]

total = sum(units for _, units, _ in PARTS)
serial = sum(units for name, units, _ in PARTS if name != "run the jobs")
fraction = serial / total

for name, units, note in PARTS:
    kind = "parallel" if name == "run the jobs" else "serial"
    print(f"{name:<22} {units:>4} unit(s)  {kind:<9} {note}")

print()
print("serial units     :", serial, "of", total)
print("serial fraction  :", f"{fraction:.4f}")
print("Amdahl ceiling   :", f"{1 / fraction:.0f}x")
print("workers anybody runs:", "tens, not hundreds")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
expand the job list       1 unit(s)  serial    Chapter 26.2, once
compile the design        1 unit(s)  serial    Chapter 25.3 §7, shared by every job
run the jobs            600 unit(s)  parallel  independent
aggregate and report      1 unit(s)  serial    Chapter 26.6, once

serial units     : 3 of 603
serial fraction  : 0.0050
Amdahl ceiling   : 201x
workers anybody runs: tens, not hundreds

A theoretical ceiling of 201× and nobody runs more than a few dozen workers. Amdahl's law does not bind a verification regression, and saying so plainly is more useful than reciting it as though it did.

Where it does bind is the diagnosis, and that is the form worth keeping:

If your measured speedup is far below your worker count, something has been serialised by accident.

A compile that is not shared is the usual culprit — remove Chapter 25.3 Section 7's cache and every job compiles, which moves the serial fraction from half a percent to most of the run. A shared lock, a single licence, one NFS directory every job writes to: each of those converts the parallel part back into a serial one, and Amdahl is how you put a number on what it cost.

4. The Limit That Does Bind

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOBS = [("smoke", 12)] * 560 + [("burst", 45)] * 35 + [("gate_soak", 360)] * 5

total = sum(m for _, m in JOBS)
longest = max(m for _, m in JOBS)

print("jobs                :", len(JOBS))
print("total work          :", total, "minutes")
print("longest single job  :", longest, "minutes")
print()
print(f"{'workers':>8} {'ideal':>8} {'floor':>8} {'efficiency':>11}")
for w in (8, 32, 128, 600):
    ideal = total / w
    floor = max(ideal, longest)
    print(f"{w:>8} {ideal:>8.0f} {floor:>8.0f} {100 * ideal / floor:>10.1f}%")

print()
print("maximum useful speedup:", f"{total / longest:.1f}x")
print("Amdahl said           :", "201x")
print("which one binds       :", "the longest job")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
jobs                : 600
total work          : 10095 minutes
longest single job  : 360 minutes

 workers    ideal    floor  efficiency
       8     1262     1262      100.0%
      32      315      360       87.6%
     128       79      360       21.9%
     600       17      360        4.7%

maximum useful speedup: 28.0x
Amdahl said           : 201x
which one binds       : the longest job

A regression cannot finish faster than its longest single job, however many workers are available. Five gate-level soaks at six hours each set a floor of six hours, and the other 595 jobs are irrelevant to it.

That is the limit Section 3 was looking for and did not find. Amdahl's law measured the wrong thing — it asks what fraction of the work is serial, and the answer was half a percent — while the binding constraint is the largest indivisible unit, which gives a real cap of 28× rather than a theoretical 201×.

Where it starts to bite is worth locating precisely:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOBS = [("smoke", 12)] * 560 + [("burst", 45)] * 35 + [("gate_soak", 360)] * 5
total = sum(m for _, m in JOBS)
longest = max(m for _, m in JOBS)

print(f"{'workers':>8} {'ideal':>7}  binding constraint")
for w in (8, 16, 24, 32, 64):
    ideal = total / w
    print(f"{w:>8} {ideal:>7.0f}  {'the long job' if longest > ideal else 'the total work'}")

crossover = total / longest
print()
print("the crossover is at:", f"{crossover:.0f} workers")
print("below it, more workers help:", True)
print("above it, more workers buy :", "nothing at all")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 workers   ideal  binding constraint
       8    1262  the total work
      16     631  the total work
      24     421  the total work
      32     315  the long job
      64     158  the long job

the crossover is at: 28 workers
below it, more workers help: True
above it, more workers buy : nothing at all

Twenty-eight workers, and the twenty-ninth buys nothing. Chapter 27.5's resource calculation will probably land below that anyway — twelve licences, say — but knowing where the ceiling is changes what buying more licences is worth.

Two consequences follow, and both are about the job list rather than about the pool.

Start the long jobs first. A scheduler that happens to run the six-hour soak last finishes six hours after everything else is done. Sorting the job list by expected duration, longest first, is the standard greedy heuristic and it costs one sorted call — Chapter 26.2 Section 4 established that the clause order decides the job order, and this is the second reason to care about it.

Split the long job, or accept the floor. A six-hour gate-level soak that could be four ninety-minute runs at different seeds has a floor of ninety minutes instead of six hours, for the same total work. That is a verification decision rather than a flow one, and it is the only thing that actually moves this limit.

5. What It Actually Buys

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BUYS = [
    ("faster feedback", "a change is tested within hours, not days"),
    ("more seeds in the same window", "Chapter 26.3's sweep can be wider"),
    ("a regression that runs nightly at all", "5 days does not fit in a night"),
]
DOES_NOT_BUY = [
    ("correctness", "the same jobs produce the same results"),
    ("reliability", "Chapter 25.7's non-determinism rate is unchanged"),
    ("fewer failures", "it finds the same bugs, sooner"),
    ("less compute", "the total work is identical"),
]

print("what concurrency buys:")
for what, why in BUYS:
    print(f"   {what:<32} {why}")
print()
print("what it does not:")
for what, why in DOES_NOT_BUY:
    print(f"   {what:<32} {why}")
print()
print("the second list is longer:", len(DOES_NOT_BUY) > len(BUYS))
print("and the first item on it is:", DOES_NOT_BUY[0][0])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what concurrency buys:
   faster feedback                  a change is tested within hours, not days
   more seeds in the same window    Chapter 26.3's sweep can be wider
   a regression that runs nightly at all 5 days does not fit in a night

what it does not:
   correctness                      the same jobs produce the same results
   reliability                      Chapter 25.7's non-determinism rate is unchanged
   fewer failures                   it finds the same bugs, sooner
   less compute                     the total work is identical

the second list is longer: True
and the first item on it is: correctness

The second entry in the first list is the underrated one. The same window now fits more seeds, so a nightly that managed one seed per test can afford five — and Chapter 26.3 Section 7 showed that distinct findings come from seed count rather than from test count. Parallelism's real contribution to quality is indirect: it buys coverage, not correctness.

The fourth entry in the second list is the one people forget when costing it. Thirty-two workers for four hours is the same compute as one worker for five days, plus overhead. Concurrency is not cheaper; it is sooner.

6. It Does Not Fix an Unreliable Job

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOBS = 600

print(f"{'non-determinism':>17} {'serial wrong':>14} {'parallel wrong':>16}")
for rate in (0.00, 0.01, 0.02, 0.05):
    wrong = rate * JOBS
    print(f"{rate:>17.0%} {wrong:>14.0f} {wrong:>16.0f}")

print()
print("the rate is a property of:", "the job, not the scheduling")
print("what parallelism changes  :", "how quickly the wrong answer arrives")
print()
print("a 2% rate over 600 jobs   :", f"{0.02 * JOBS:.0f} wrong results")
print("delivered serially in     :", "5 days")
print("delivered in parallel in  :", "4 hours")
print("improvement in accuracy   :", "none")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  non-determinism   serial wrong   parallel wrong
               0%              0                0
               1%              6                6
               2%             12               12
               5%             30               30

the rate is a property of: the job, not the scheduling
what parallelism changes  : how quickly the wrong answer arrives

a 2% rate over 600 jobs   : 12 wrong results
delivered serially in     : 5 days
delivered in parallel in  : 4 hours
improvement in accuracy   : none

The columns are identical. Chapter 25.7 established that a non-determinism rate is a property of the machinery, measured by running one test many times at one seed; nothing about running six hundred different jobs at once changes it.

This is why Chapter 25.7 came before this module rather than after. The soak drives the rate to zero first, and parallelism then multiplies a trustworthy result. Applied in the other order it multiplies an untrustworthy one — twelve wrong results per night instead of twelve per five nights, which is a worse situation arriving faster.

7. And It Can Make Things Worse

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
HAZARDS = [
    ("two jobs in one directory", "unreachable", "reachable", "Chapter 26.4 §2"),
    ("a shared log file", "unreachable", "reachable", "Chapter 26.4 §2"),
    ("licence pool exhausted", "impossible with 1 job", "routine", "Chapter 27.5"),
    ("memory exhausted", "one job's footprint", "n jobs' footprint", "Chapter 27.5"),
    ("an exception inside a worker", "propagates", "can be swallowed", "Chapter 27.4"),
    ("results arriving out of order", "never", "always", "Chapter 27.4"),
]

print(f"{'hazard':<30} {'serial':<22} {'parallel':<20} where")
for hazard, serial, parallel, where in HAZARDS:
    print(f"{hazard:<30} {serial:<22} {parallel:<20} {where}")

print()
newly = sum(1 for _, s, _, _ in HAZARDS if s in ("unreachable", "never", "impossible with 1 job"))
print("hazards that did not exist serially:", newly, "of", len(HAZARDS))
print("hazards parallelism removes        :", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hazard                         serial                 parallel             where
two jobs in one directory      unreachable            reachable            Chapter 26.4 §2
a shared log file              unreachable            reachable            Chapter 26.4 §2
licence pool exhausted         impossible with 1 job  routine              Chapter 27.5
memory exhausted               one job's footprint    n jobs' footprint    Chapter 27.5
an exception inside a worker   propagates             can be swallowed     Chapter 27.4
results arriving out of order  never                  always               Chapter 27.4

hazards that did not exist serially: 4 of 6
hazards parallelism removes        : 0

Four new hazards, none removed. That is the honest ledger, and it is why Chapter 27.4 exists as a chapter of its own.

The first two are Chapter 26.4's, and the relationship is worth stating precisely: Module 26 fixed them before they could happen. A serial regression with a shared directory works perfectly, so the bug is latent — it is created by the directory naming and activated by concurrency. Chapter 26.4 was marked Critical for a reason that only becomes visible here.

The last row is the one that surprises people. Results arrive in completion order, not submission order, so anything that assumed the job list's order — a progress log, a comparison against yesterday by position, a report that walks results and tests in parallel — is now wrong. Chapter 26.6's aggregation survives because it reconciles by job identity rather than by position, which was not an accident.

8. The Decision

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def should_parallelise(serial_days, window_hours, non_determinism_rate,
                       directories_isolated):
    reasons = []
    if serial_days * 24 <= window_hours:
        reasons.append("it already fits in the window")
    if non_determinism_rate > 0:
        reasons.append(f"non-determinism is {non_determinism_rate:.0%} — fix that first (25.7)")
    if not directories_isolated:
        reasons.append("jobs are not directory-isolated (26.4)")
    return (not reasons), reasons


CASES = [
    ("ready", 5.0, 10, 0.00, True),
    ("already fits", 0.3, 10, 0.00, True),
    ("unreliable", 5.0, 10, 0.02, True),
    ("not isolated", 5.0, 10, 0.00, False),
]

for name, days, window, rate, isolated in CASES:
    ok, reasons = should_parallelise(days, window, rate, isolated)
    print(f"{name:<14} parallelise={str(ok):<6} {reasons if reasons else ''}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ready          parallelise=True   
already fits   parallelise=False  ['it already fits in the window']
unreliable     parallelise=False  ['non-determinism is 2% — fix that first (25.7)']
not isolated   parallelise=False  ['jobs are not directory-isolated (26.4)']

Three preconditions, and the middle one is the one teams skip.

It must not already fit. A regression that finishes in seven hours against a ten-hour window gains nothing from concurrency except Chapter 27.4's hazards. Speed is worth having when it changes what is possible — nightly instead of weekly — and not otherwise.

The non-determinism rate must be zero, by Chapter 25.7's soak. Parallelising first means every later investigation has two candidate explanations, and the farm is always the more comfortable one to blame.

The jobs must be directory-isolated, which Module 26 already did. If they are not, Chapter 26.4 is the prerequisite and not an optimisation.

The function returns reasons rather than a boolean alone, which is Chapter 24.5's pattern: a decision that explains itself can be argued with, and one that does not gets overridden silently.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def amdahl(s, n):
    return 1 / (s + (1 - s) / n)


J, M = 600, 12
serial = J * M

# A
print("A:", serial, f"{serial / 1440:.1f}")

# B
print("B:", f"{serial / 32 / 60:.2f}")

# C
print("C:", f"{amdahl(0.0, 32):.1f}")

# D
print("D:", f"{amdahl(0.05, 32):.1f}", f"{amdahl(0.05, 128):.1f}")

# E
print("E:", f"{1 / 0.05:.0f}")

# F
print("F:", f"{3 / 603:.4f}", f"{603 / 3:.0f}")

# G
print("G:", 0.02 * J, 0.02 * J)

# H
print("H:", 32 * 3.75, 1 * 120)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 7200 5.0
B: 3.75
C: 32.0
D: 12.5 17.4
E: 20
F: 0.0050 201
G: 12.0 12.0
H: 120.0 120

A — the serial total, in minutes and days.

B — thirty-two workers, under four hours.

C — with nothing serial, speedup equals the worker count exactly.

D — with five percent serial, quadrupling the workers buys less than five times.

E — and the ceiling is twenty whatever you do.

F — a regression's actual serial fraction, and its ceiling: not the binding constraint.

G — the wrong-result count, serial and parallel. Identical.

H — and the total compute is identical too: concurrency is sooner, not cheaper.

10. Exercises

Exercise 1 — your own number. Compute your regression's serial duration from its job count and a typical per-job time. Report runs per week.

Exercise 2 — the archive. Say at what point a regression stops being a safety net for your team's change rate.

Exercise 3 — the table. Produce the worker/hours/speedup table for your own job count.

Exercise 4 — the assumption. Name the Module 26 chapters that make the linear speedup column plausible.

Exercise 5 — Amdahl. Compute the speedup at 8, 32 and 128 workers for serial fractions of 1%, 5% and 20%.

Exercise 6 — your serial part. List what in your flow runs once, and compute the fraction.

Exercise 7 — the ceiling. Report your Amdahl ceiling and compare it with the worker count you would actually use.

Exercise 8 — the diagnosis. State the useful form of Amdahl's law for a regression, in one sentence.

Exercise 9 — accidentally serialised. Name three things that could serialise your parallel part, and say what each would do to the fraction.

Exercise 10 — the floor. Find your suite's longest single job and compute the maximum useful speedup as total over longest.

Exercise 11 — the crossover. Work out the worker count above which more workers buy nothing.

Exercise 12 — two caps. Compare your Amdahl ceiling with your makespan floor and say which binds.

Exercise 13 — longest first. Sort your job list by expected duration and say what it saves, citing Chapter 26.2.

Exercise 14 — split it. Identify a long job that could be several shorter ones and compute the new floor.

Exercise 15 — mean or max. Say which of the two sets the floor and which you budget with.

Exercise 16 — two lists. Write out what concurrency buys and does not buy for your flow.

Exercise 11 — coverage, not correctness. Explain parallelism's indirect contribution to quality, citing Chapter 26.3.

Exercise 12 — not cheaper. Compute the total compute for your serial and parallel plans and compare.

Exercise 13 — the identical columns. Show that a non-determinism rate produces the same number of wrong results either way.

Exercise 14 — order of operations. Say why Chapter 25.7's soak must come before this module.

Exercise 15 — the comfortable explanation. Describe how twelve spurious failures a night get attributed to the farm.

Exercise 16 — the ledger. List the hazards concurrency adds for your flow and the ones it removes.

Exercise 17 — latent, then activated. Explain why a serial regression with a shared directory works perfectly.

Exercise 18 — out of order. Find something in your flow that assumes the job list's order.

Exercise 19 — three preconditions. Check all three for your regression and report which fail.

Exercise 20 — reasons, not a boolean. Say why the decision function returns its reasons.

11. Summary

  • A five-day regression runs 1.4 times a week, which is an archive rather than a safety net.
  • Every figure here is arithmetic over configured inputs; Chapter 27.2 is about measuring your own.
  • The annotation's arithmetic holds: 600 × 12 minutes is five days, and 32 at a time is 3.75 hours.
  • The speedup column is linear because the jobs are independent, which Module 26 spent nine chapters making true.
  • Amdahl's law caps speedup at the reciprocal of the serial fraction, regardless of worker count.
  • It does not bind a verification regression: a serial fraction of 0.5% gives a ceiling of 201×, and nobody runs that many workers.
  • Its useful form is a diagnosis — a measured speedup far below the worker count means something was serialised by accident, usually an unshared compile.
  • A regression cannot finish faster than its longest single job, whatever the worker count.
  • That floor is the limit that actually binds — a real cap of 28× against Amdahl's theoretical 201×.
  • Above the crossover, more workers buy nothing at all, which changes what more licences are worth.
  • Start the long jobs first: a six-hour job scheduled last finishes six hours after everything else.
  • Splitting a long job into several shorter ones is the only thing that moves the floor, and it is a verification decision rather than a flow one.
  • The mean per-job duration is what you budget with; the maximum is what sets the floor.
  • Concurrency buys faster feedback and more seeds in the same window, and Chapter 26.3 showed that seeds are where distinct findings come from.
  • It does not buy correctness, reliability, fewer failures or less compute. It is sooner, not cheaper.
  • A non-determinism rate is unchanged by parallelism — twelve wrong results either way, delivered in four hours instead of five days.
  • Chapter 25.7's soak must come first, or every later investigation has the farm as a comfortable alternative explanation.
  • Concurrency adds four hazards and removes none.
  • Chapter 26.4's directory collision was latent and is activated here — a serial regression with a shared directory works perfectly.
  • Results arrive in completion order, so anything that assumed the job list's order is now wrong; Chapter 26.6 survives because it reconciles by identity.
  • Three preconditions: it must not already fit the window, the non-determinism rate must be zero, and the jobs must be directory-isolated.
  • Return the reasons, not just the verdict, so the decision can be argued with.

The next chapter measures the serial regression properly, so that every later comparison has something to be compared against.

Where this fits

Part of the Python curriculum.