Parallelization Basics
Before you scale a job across the cluster, it helps to picture what “in parallel” actually means, when it works, and why it’s usually the single biggest speedup available to you.
When Parallelization Helps
Think back to the kitchen from Day 3: your machine is a kitchen, and every CPU core is a burner.
Say you want four grilled cheeses. The steps within one sandwich mostly don’t split: you can’t grill a side before the cheese is on the bread — nearly every step needs the previous one finished. Put four cooks on a single sandwich and three of them stand around watching. And no matter how many cooks you hire, a sandwich that takes four minutes takes four minutes.
But the sandwiches don’t depend on each other. Each is its own pan — get the bread, put on the cheese, grill one side, grill the other — and no sandwich needs anything from the others. Fire up four burners and lunch is ready in a quarter of the time.
Code is the same. Parallelization pays off when the pieces of work are independent — each can run without waiting on the results of another. When a job splits cleanly into fully independent tasks with no coordination between them, it’s called embarrassingly parallel — the easiest, highest-payoff kind of work to parallelize.
A quick test, in kitchen terms: if you could hand each task to a different cook and never have them talk to each other, it will parallelize. Your extraction job fits perfectly — every filing is its own self-contained task.
Parallelization doesn’t make a single task faster — one grilled cheese still takes its four steps. It makes many tasks finish sooner by running them at the same time. If your bottleneck is one slow step, parallelizing won’t help; you need a faster step.
Let’s run through some examples together and discuss what types of tasks are parallelizable (or useful to parallelize):
Example 1
You’re given an array of numbers, and you’re asked to compute the sum of the numbers in the array:
Is parallelizing possible and/or worthwhile?
What if we want to compute the sum of some complicated function of each number (see below)?
Example 2
You have a dataset A where rows are meant to have a unique key. You want to merge dataset B onto A, but without the merge creating new rows. A standard way to avoid that is to check that A’s rows are unique. Can this be parallelized?
Example 3
You’re scraping a website to compile a dataset. After scraping each page, you want to write some variables you found on the page to a common .csv that will store the results from all the webpages you’ve scraped. Can this be parallelized?
🔮 How to cast your progress — click to reveal
Every quest you check reveals a 🔮 Cast to the leaderboard button with a one-line ./cast spell. To record it:
- SSH to the Yens —
ssh SUNetID@yen.stanford.edu cdto your repo —cd ~/gsb-research-computing-ai-skills- Paste the
./cast …spell from the quest and run it.
Ways to Parallelize
Let’s make this concrete with the job you ran yesterday: processing SEC filings. Each filing is a sandwich: fetch it, send it to the API, save the fields that come back — those steps run in order, and no filing needs anything from another. So 100 filings are 100 independent sandwiches. There’s more than one way to spread them out: give one job more cores, run more jobs, or do both.
Recall yesterday’s script. It ran as one job on one core — a for loop walking the filings in sequence:
for filing in filings: # 100 filings in the list
result = extract(filing) # one core does this, start to finish
save(result)
# the loop can't start filing 2 until filing 1 is done
If one filing takes 5 seconds, 100 filings take ~500 seconds — and the whole time your script is using exactly one core. A Yen node has dozens more you could have asked for.
Watch as we run this baseline and each of the three approaches below on the Yens — 20 filings every time, so the only thing that changes is how the work is spread. (The diagrams use 8 filings to stay legible; the live runs do 20.) This first one is the baseline: 1 job, 1 core, 20 filings, 20 API calls.
After each run,
watch sacct -X -j JOBID --format=JobID,State,Elapsed,TotalCPU,ReqCPUS,MaxRSSshows what it cost:Elapsedis the wall-clock time,TotalCPUagainstReqCPUSshows how busy the reserved cores actually stayed, andMaxRSSis the peak memory — the same field you compared against your estimate on Day 3.
Approach 1: One job, many cores — parallelize within a job. Ask the same job for several cores (on the Yens, set #SBATCH --cpus-per-task in your .slurm script) and split the filings across them in your code. But you’re capped at the cores on a single machine:
Two cores clear the eight filings in four waves — ≈ 4 × 5s = 20s of wall-clock, versus ~40s one at a time. Same total work, spread across two cores.
Ask Claude Code for help with parallelizing within a job. Get it to split the filings across the cores for you: describe your loop and say how many cores you asked for. Read what it gives you before you run it — check that the work really is independent.
Let’s run an example like this on the Yens and see how it performs — the same 20 filings as the baseline, now split across the cores of a single job: 1 job, 2 cores, 20 filings, 20 API calls.
Approach 2: Many jobs, one core each — parallelize across jobs. Submit a job array: the scheduler launches many near-identical jobs at once, each an independent task on (possibly) a different node, each working its own slice of the filings. This scales past a single machine, and because every task stands alone, a failure costs you only that task:
We’ll cover job arrays in detail on the next page.
The tasks are identical — so you have to tell them apart. Every task in an array runs the same script, which means nothing decides on its own which filing each one takes. That mapping is yours to write.
Let’s run an example like this on the Yens and see how it performs — the same 20 filings again, this time split across independent jobs: 2 jobs, 1 core each, 10 filings apiece, 20 API calls.
Approach 3: Many jobs, many cores — do both. Nothing stops an array task from itself requesting several cores. Reach for this when one alone isn’t enough: many jobs to spread across nodes, several cores inside each to chew through a big slice:
Let’s run an example like this on the Yens and see how it performs — the same 20 filings once more, now spread across both jobs and cores: 2 jobs, 2 cores each, 10 filings apiece, 20 API calls.
Putting the four possibilities side by side:
| One core per job | Many cores per job | |
|---|---|---|
| One job | Day 3’s baseline — a for loop, one filing at a time. No speedup. |
Approach 1. --cpus-per-task=N, then split the filings across the cores in your code. Capped at one node. |
| Many jobs | Approach 2. A job array. Scales past one node, and one task failing costs you only that task. The number of tasks you can queue is capped. | Approach 3. A job array whose tasks each ask for several cores. The most throughput, but the largest request — and the longer the queue wait. |
More Filings Than Cores (and Vice Versa)
The illustrations above show a tidy few-filings-per-core picture. In practice the ratios vary, and both directions come up constantly.
More filings than cores — the usual case. Say you have 100 filings but only 8 cores. The cores work in waves: the first 8 filings run at once, and the moment a core finishes it picks up the next filing in line, until all 100 are done. The total time is roughly the number of waves times the per-filing time — about ⌈100 ÷ 8⌉ = 13 waves × 5s ≈ 65s, versus ~500s in serial. You don’t manage the waves yourself: within a job your code hands out the next filing; across array jobs, the scheduler does.
More cores than filings. Say you have 5 filings but the same 8 cores. Only 5 cores can do anything — a single filing can’t be split across cores — so the other 3 sit idle. Cores beyond the number of independent tasks buy you nothing, and requesting more than you’ll use can mean a longer wait in the queue for resources you never touch. Once the job starts, Slurm holds those cores for you whether you use them or not — so they sit idle in your job instead of running someone else’s. The Yens are shared; ask for what you’ll actually use.
Hitting a wall on resources in the future? Ask us. If a job is too slow, or you’re bumping up against what you can reasonably request, the DARC team is happy to look at your code with you.
What You Learned
- You can explain the difference between running tasks sequentially on one core and in parallel across many
- You can tell whether a workload is independent enough to parallelize
- You can explain a few ways to parallelize — more cores within one job, more jobs, or both — and name the limit each one runs into
- You understand that parallelization speeds up many tasks, not a single slow one