Computational Economics

Unit 04 · Eight short lessons, three of them optional

Parallel computing and acceleration

Make the same model computation cheaper. Each short lesson starts with fixed inputs, makes one change, checks what must stay the same and ends with a result you can write down. Each has a one-minute film and points to the code that does the work. Lessons 01, 02, 03, 06 and 08 are the core. Lessons 04, 05 and 07 are optional: their code uses GPU kernels written in Triton, and you can skip them on a first reading.

The code independently implements an economy based on De Loecker, Eeckhout and Mongey (2026). The acceleration examples and benchmark measurements were produced for this course.

References and measured results

Read the complete unit on one page

Unit 04 · Lesson 01

Parallel work

Run eight independent tasks in parallel while keeping each task’s work unchanged.

Given
Eight jobs with fixed durations. Each job stands for solving one candidate economy.
Change
Choose 1, 2, then 4 workers. Move the clock to the end of each schedule; then repeat with Add serial work checked.
Check
Each job still runs once and keeps its duration. Observe overlapping jobs, idle lanes near the end, and the setup and join that all workers must wait for.
Record
A six-row table: workers, serial work on/off, finish time and speedup. Give one reason the gain need not equal the worker count.

Same eight jobs. More overlap.

Durations are in work units, not seconds.

Loading the saved illustration.
Selected finish
—
1 worker: —
Speedup vs 1 worker
—
Same jobs and serial setting
Completed tasks
0 / 8
Still queued: 8
QueuedRunningDone

The top row is always one worker; below it, the same jobs run on the workers you choose. Every job keeps its duration; only the overlap changes.

Go straight to the GPU batching film →

Code on GitHub lessons/04_parallel/scaling.py ↗ · 07_python/qmp/estimation.py ↗ · lessons/03_estimation/full_search.py ↗

01 / Execution

Five ways to organize the work

Follow each change from the work it saves to the work that remains. Each film is one minute, with English narration and captions.

Parallel workers

Each candidate economy has its own parameters and returns its own moments. Give independent economies to separate workers: a worker can start its next job while the others are still solving theirs.

What is removed?
The wait for one independent economy to finish before another can begin.
What remains?
Every economy must still be solved in full. Setup, the final join and uneven job lengths can limit the gain.
# Each call solves one independent economy.
results = pool.map(solve_economy, candidates)
moments = collect_in_candidate_order(results)

Try the worker illustration above ↑

Find this in the source

Download the current source. In 07_python/qmp/estimation.py, solve assigns rows to one serial shard and writes each result by row_id, and select checks the combined records. lessons/03_estimation/full_search.py runs several shards side by side, and lessons/04_parallel/scaling.py solves economies in a pool of worker processes, as in the pool.map example above.

60-second explanation · EnglishParallel workers: the same jobs, a shorter clock

Watch independent jobs leave a shared queue, overlap on worker lanes and meet at the final join. Then add the work that cannot overlap.

Check your result
WorkersSerial workFinishSpeedup
1off24 units1.00×
2off13 units1.85×
4off7 units3.43×
1on27 units1.00×
2on16 units1.69×
4on10 units2.70×

Two reasons the gain falls short of the worker count. The jobs have different lengths, so lanes stand idle near the end: if the 24 units of work split evenly, four workers would finish at 6 units, but the last job ends at 7. And the 2 units of setup and 1 unit of join take the same time with any number of workers, so with serial work the speedup can never exceed 27 / 3 = 9.

Vectorization

Put firms in rows and markets in columns. One array expression applies the same operation across the matrix; a sum along the firm axis still gives a separate total for each market.

Given
One economy’s firm values, arranged as M rows and J market columns, with inactive firms masked out.
Change
Follow the film from separate market calls to one matrix operation. Trace one column through the firm sum and normalization in the code below.
Check
The sum runs down firms, not across markets. Every nonempty market’s shares sum to one; each market keeps its own convergence state.
Record
Label the shapes of the input, market totals and output. Explain which repeated calls disappear and which arithmetic remains.
# weights has shape (firms, markets).
market_totals = weights.sum(axis=0, keepdims=True)
shares = weights / market_totals

Vectorization changes how the work is written. This array code still runs on one CPU core; workers and GPUs are separate steps, in lessons 01 and 03.

Time it: python lessons/04_parallel/loop_vs_vectorized.py solves the same 2,000 markets in one call and in one call per market, with 5 to 80 firms per market, in about 30 seconds.

Find this in the source

In the current source, open 07_python/qmp/equilibrium.py → solve_shares → its nested update. Find weight.sum(axis=0), then the working = ~done mask. This is the complete share iteration; the normalization shown here is only one operation within it.

Pause when the matrix appears. Point to one market column, then follow its total into its normalized shares.

60-second explanation · EnglishVectorization: from market calls to an array

Follow four market calls into a firms-by-markets matrix. Watch the column sums and normalization preserve each market's boundary.

Check your result
  • Shapes. Weights (M, J): one row per firm, one column per market. Market totals (1, J) with keepdims=True. Shares (M, J).
  • Reduction axis. axis=0, down the firms of each market, so each column keeps its own total. A sum along axis 1 would mix markets.
  • What disappears. The J separate calls to the share solver, one per market, and the overhead of each call. What remains. The same arithmetic for every firm, the firm sum in each market and the share iterations. Each market keeps its own convergence test: working = ~done freezes the markets that have converged while the others continue.
  • Timing. Both versions give the same shares up to rounding and the same iterations in every market. The loop's time hardly changes with M; the vectorized time grows with it. So the speedup falls as M rises: on one core, from about 240 times with 5 firms to about 20 times with 80. Your times will differ, but the speedup will fall the same way.

Code on GitHub 07_python/qmp/equilibrium.py ↗ · lessons/04_parallel/loop_vs_vectorized.py ↗

GPU batching

Stack compatible economies into a B × M × J array. A GPU operation can then work across many economies and markets, with a separate active mask for the parts that still need to converge.

Given
Fixed candidate rows and saved draws. Economies with the same M = floor(N) can share a B × M × J batch.
Change
Follow the film as separate economy matrices enter one batch. Give each launch more work, and keep track of which parameters and result belong to which economy.
Check
A finished market stops updating while others continue. Every economy still goes through both periods, every exit round and every root solve. The batch must fit in GPU memory.
Record
Draw the three axes and mark the active mask. Propose two batch sizes to compare, with the same rows and checks, and a clock that covers the whole solve.
# B economies × M firms × J markets.
state = stack_compatible_economies(economies)
state = update_active_markets(state, active_mask)

A larger batch helps while it fits in memory and keeps the GPU busy. Compare batch sizes by timing the whole solve.

Find this in the source

In the current source, follow 03_gpu/run_points_current.py → groups → solve_rows, then 03_gpu/qmp_torch_current.py → solve_batch_current. groups chooses B from the cell budget; solve_rows restores the requested row order. Read docs/GPU.md for how to run it and what it checks.

Pause at the stacked array. Identify B, M and J; then watch which parts remain active after others finish.

60-second explanation · EnglishGPU batching: give one launch more work

See economy matrices form a batch, enter a GPU together and retain their own convergence state.

Check your result
  • The axes. B economies with the same M, M firms and J markets. The active mask has one entry for each economy and market, so a market that has converged in one economy stops updating while the same market in another economy continues.
  • A comparison plan. Solve the same rows with two batch sizes, for example a small one and the largest that fits in GPU memory, with the same draws, precision, root and exit rules. Record the time of the whole solve, with the GPU synchronized before the clock stops, the peak memory, and agreement with the CPU results row by row.
  • The trade-off. A larger B gives each launch more work and needs fewer launches, but it uses more memory. When a batch does not fit, reduce B and keep every requested row.

Code on GitHub 03_gpu/run_points_current.py ↗ · 03_gpu/qmp_torch_current.py ↗

Kernel fusion

Optional lesson: its code uses GPU kernels written in Triton. The core lessons are 01, 02, 03, 06 and 08.

A chain of small GPU operations can write a temporary array to device memory, then read it back in the next operation. A fused kernel combines compatible operations so intermediate values can stay inside the kernel.

Given
The same input array and a chain of compatible operations. The unfused version writes an intermediate array to device memory.
Change
Trace the separate operations in the film, then the fused kernel. Cross out the temporary-array write/read and launch that fusion can remove.
Check
The required arithmetic and reductions still happen. Retain the intended precision, damping, masks and residual checks; compare outputs before timing.
Record
A before/after diagram of launches and memory traffic. Name the saving for the share update alone.
# Separate operations materialize a temporary array.
temporary = scale * x
y = temporary + offset

# A compatible fused kernel computes the same expression.
y = fused_scale_and_add(x, scale, offset)

Fusion changes how the work runs and how much it reads and writes in memory.

Find this in the source

In the current source, compare 03_gpu/qmp_torch_current.py::_share_step with 03_gpu/qmp_torch_share_fused.py::share_step_fused and _share_step_kernel. The real kernel fuses one whole share update, including the sums over firms; the film’s scale-and-add example is a simpler case. The convergence loop stays outside the kernel.

Pause on each version. Count the depicted launches and temporary-memory trips; keep the required operations on both drawings.

60-second explanation · EnglishKernel fusion: fewer trips to memory

Trace reads, intermediate writes and launches before and after combining a compatible chain of operations.

Check your result
  • Before. Two launches. The first reads x and writes the temporary array to device memory; the second reads the temporary back and writes y.
  • After. One launch reads x and writes y. The temporary value stays inside the kernel, so one launch, one write and one read of a full array disappear. The multiplication and the addition both still happen.
  • The checks. On the same inputs, shares, markups, masks and residuals agree with the unfused share step within the original tolerances. Then time the full solve, once the checks pass: only the share update gets cheaper, so the full solve speeds up by less than the share step.

Code on GitHub 03_gpu/qmp_torch_current.py ↗ · 03_gpu/qmp_torch_share_fused.py ↗

Resident iteration

Optional lesson: its code uses GPU kernels written in Triton. The core lessons are 01, 02, 03, 06 and 08.

When the host checks every small iteration, it may repeatedly wait for the GPU and launch the next step. A resident inner loop keeps state and compatible stopping checks on the device through that sequence.

Given
The same share state, FP32 predictor and FP64 correction budget as the mixed backend.
Change
Follow one correction step in each film lane. Move the correction loop and its stopping state onto the GPU, reducing host checks and relaunches.
Check
Each step still uses the previous state. Preserve each market’s first stopping step, masks, iteration cap, failure path and independent final FP64 residual.
Record
Mark where state lives and where the host waits in both versions. Name one removed handoff and one dependency that remains.
# Inside the resident kernel, with the original stopping rules:
while any(active) and iteration < iteration_cap:
    state = update_active_markets(state, active)
    active = original_remaining_work_mask(state)
    iteration += 1
return state, convergence_status

Only the inner share loop moves onto the GPU; the output root, the exit rounds and the rest of the economy run as before. Each update still needs the one before it.

Find this in the source

In the current source, compare 03_gpu/qmp_torch_precision.py::solve_shares_mixed with 03_gpu/qmp_torch_share_resident.py::solve_shares_resident_mixed → resident_updates → _resident_kernel. Both return to the independent _share_residual check. The optional resident_backend is documented in docs/GPU.md.

Pause at a host/device handoff, then at the resident loop. Follow the same state through two consecutive updates.

60-second explanation · EnglishResident iteration: keep the state near the work

Compare repeated host/device handoffs with an inner loop whose state and stopping logic remain on the GPU.

Check your result
  • Host-driven. The share state stays on the GPU, but at each stopping check, every fourth update in the mixed backend, the host waits for the GPU, reads the result and launches the next updates.
  • Resident. The loop and its stopping checks run on the GPU. The host launches it once and waits once, at the end.
  • Removed handoff. The host’s wait and relaunch at each stopping check. Remaining dependency. Each update still needs the state from the previous one, and the returned shares still pass the independent FP64 residual check before prices, the output root and the exit decision use them.

Code on GitHub 03_gpu/qmp_torch_precision.py ↗ · 03_gpu/qmp_torch_share_resident.py ↗ · 03_gpu/qmp_torch_current.py ↗

For measured times on this model, see the GPU measurements.

Explore the six implementation layers Optional reference: arrays, roots, workers and devices
One formula over all columnsOne economy · one CPU worker
Firms ↓J markets →
Market 1Firm 1Firm 2Firm 3Local sum ↓
Market 2Firm 1Firm 2Firm 3Local sum ↓
Market 3Firm 1Firm 2Firm 3Local sum ↓
… JFirm 1Firm 2Firm 3Local sum ↓
shares = weights / weights.sum(axis=0)

A matrix also runs on a CPU. Each column keeps its own convergence test.

Start with one market per call. Put markets in a CPU array. Add independent economies to a GPU batch.

Code on GitHub lessons/04_parallel/output_root.py ↗ · 07_python/qmp/equilibrium.py ↗

02 / The acceleration workflow

Profile, change, validate

Choose a specific cost, change one part of the implementation, then check the answer before comparing times.

Read a profile before changing code.

Find the work that keeps the GPU busy, then compare it with the time you wait for the whole solve.

Given
The film’s shared clock and a recorded profile of the mixed-precision solver. Its percentages are shares of GPU activity.
Change
Read the host and GPU lanes together. Locate the expensive share-correction region, then choose one launch, memory or arithmetic cost to investigate.
Check
Host waiting can overlap GPU work, so count overlapping time once. An activity percentage is a share of GPU work, not of the time you wait.
Record
Three sentences: the cost you observed, one change to try, and how you would check the answer and time the change without the profiler.

Pseudocode

# Inspect a representative complete solve.
with profile_cpu_and_gpu():
    solve_complete_batch(frozen_inputs)

# Change one expensive region, validate, then time without the profiler.

Use the profile to choose an experiment, and a timed run to judge it.

Find this in the source

The current source provides the call to profile: 03_gpu/qmp_torch_current.py::solve_batch_current. Its _period calls solve_shares_current, and a precision setting that you switch on selects the mixed implementation. Read docs/GPU.md before you wrap the call in a profiler. The profiling code on this page is pseudocode; the download has no profiler command.

Pause where host waiting overlaps GPU activity. Mark the shared interval once before reading the hotspot percentage.

60-second explanation · EnglishRead a profile: activity and elapsed time

Separate device work from host waiting and use a saved profile to choose the region worth investigating.

Check your result
  • The shared interval. While the GPU computes, the host waits for the same work. Count that interval once: adding the two lanes counts it twice.
  • The observed cost. In the saved mixed-backend profile, FP64 share correction accounts for about 64% of recorded GPU activity. That is a share of GPU work, not of the time you wait for the whole solve.
  • One possible experiment. Change one thing in the correction, for example fuse its share update (lesson 04) or keep its loop on the GPU (lesson 05). Check that the same saved cases give the same shares, residuals, operating sets, exit paths and moments. Then compare repeated, warmed timings of the whole solve without the profiler, with the GPU synchronized before the clock stops.

Code on GitHub 03_gpu/qmp_torch_current.py ↗

Predict cheaply, then correct strictly.

Optional lesson: its code uses GPU kernels written in Triton. The core lessons are 01, 02, 03, 06 and 08.

A low-precision share prediction supplies a starting point. The high-precision correction and independent final check decide whether the answer is acceptable.

Given
The same FP64 inputs and final acceptance rules. Only a preliminary share prediction may use FP32.
Change
Follow the prediction into FP64 correction and a fresh residual check. Trace the fallback to the original FP64 initialization when the prediction is unusable.
Check
Deduct the predictor budget from the total cap. Retain the original correction tolerance; prices, roots, fixed costs, exit decisions and moments remain FP64.
Record
Annotate the flow with each stage’s precision and acceptance check. Explain why a small FP32 predictor residual cannot certify the final answer.

Pseudocode

prediction = predict_shares_fp32(inputs)
start = usable_fp64_start_or_original(prediction)
shares = correct_shares_fp64(start, original_tolerance, remaining_budget)
check_independent_fp64_residual(shares)
# Continue prices, roots, exits and moments in FP64.

The predictor can change the path and the number of iterations compared with an all-FP64 solve. The FP64 checks decide whether the final result is accepted.

Find this in the source

In the current source, read 03_gpu/qmp_torch_precision.py::_fp32_predict and solve_shares_mixed. Find usable, budget and the final _share_residual call. precision_backend is a with block that switches the mixed solver on for the code inside it; importing the module leaves the default solver unchanged.

Pause at the FP32-to-FP64 transition. Identify what is reused, what is checked again, and where a failed prediction goes.

60-second explanation · EnglishMixed precision: a predictor is not the final answer

Follow an FP32 share prediction into FP64 correction, a fresh residual check and the remaining FP64 economy.

Check your result
  • FP32 prediction. At most 16 share updates with a looser tolerance. It only proposes a starting point.
  • Usable start, FP64. The prediction is converted to FP64 and normalized. Where its shares or market totals are not finite and positive, the market restarts from the original FP64 initialization.
  • FP64 correction. The original update rule and tolerance, with the 16 predictor updates deducted from the iteration cap.
  • Independent FP64 check. The residual of the returned shares is computed again and must meet the original tolerance, together with the recorded stopping residual, the iteration cap and finiteness. Prices, the output root, fixed costs, exit decisions and moments stay in FP64.
  • Why FP32 cannot certify the answer. FP32 keeps about seven significant digits, so its residual cannot show that the shares meet the FP64 tolerance of 10−10. Only the FP64 check of the returned shares can.

Code on GitHub 03_gpu/qmp_torch_precision.py ↗ · 03_gpu/qmp_torch_current.py ↗

Validate, then time the same work.

Keep the inputs, the hardware and the points where the clock starts and stops fixed. Do the preparation and the checks before the timed runs.

Given
Recorded times for the same 256 economies: three warmed runs of each configuration, and a 64-core CPU run on the same machine.
Change
Compare 1 and 4 GPUs within one precision mode first. Then compare FP64 and mixed on the same GPU count, changing one factor at a time.
Check
Use the same economies, draws, checks and clock. The clock excludes preparation and compilation; it includes transfers, the whole solves and the returned results.
Record
A comparison of the medians and observed ranges, with the hardware and what the clock excludes. Say which configuration finished sooner for this workload. In a new experiment, keep every repeated time.

Pseudocode

validate_answers_and_exit_paths(frozen_cases)
prepare_workers_compile_and_warm_up()
for repetition in range(3):
    synchronize_devices()
    with complete_batch_clock():
        dispatch_solve_gather(frozen_inputs)
        synchronize_devices()
save_all_timings_and_provenance()

The film uses the comparison in GPU measurements. The page’s other timings, of the share block and of the CPU workers, each start and stop the clock at different points; compare each only with itself.

Find this in the source

Read the timing note and the timing summaries. The summaries give medians, ranges and the number of checked results; the logs of each run are not included. In the current source, inspect 03_gpu/run_points_current.py::solve_rows for ordered results and GPU synchronization, and 07_python/qmp/estimation.py::diagnostic_quality for acceptance checks. The runner’s own timer measures one batch; the benchmark clock covers the whole run across workers.

Pause where the clock stops. List what must finish before it stops, then compare the recorded results.

60-second explanation · EnglishA fair comparison: one workload, one clock

Fix the workload, check answers and exit paths, warm up, then compare three repeated timings with the same clock.

Check your result

Median and observed range of three warmed repetitions for the same 256 economies, on one machine with an Intel Xeon Gold 6530 and NVIDIA L40S GPUs:

ConfigurationMedianObserved range
FP64, 1 GPU23.105 s23.104–23.111 s
FP64, 4 GPUs7.120 s7.078–7.181 s
Mixed, 1 GPU17.520 s17.499–17.577 s
Mixed, 4 GPUs5.605 s5.578–5.635 s
CPU, 64 cores, FP6435.869 s35.575–35.873 s

Within one mode, four GPUs finish 3.25 times sooner than one in FP64, and 3.13 times sooner with mixed precision. At a fixed GPU count, mixed precision finishes sooner: 17.520 s against 23.105 s on one GPU. For this workload, four GPUs with mixed precision finish first, at 6.40 times the throughput of the 64-core CPU run. All 5,376 timed records passed the checks. The clock excludes imports, worker start-up, draw setup, compilation and warm-up.

Code on GitHub 03_gpu/run_points_current.py ↗ · 07_python/qmp/estimation.py ↗

03 / Measured GPU results

Compare the same complete workload

256 fixed economies, both periods and three moments. The 64-core CPU run and the runs on 1, 2 or 4 GPUs used the same machine and the same clock; each configuration ran three times after a warm-up.

CPU · 64 physical cores · FP64: 35.869 s median · 35.575 s–35.873 s observed

Fixed 256-economy workload, with one, two and four GPUs in two precision modes
CardsFP64
throughout
FP32 share predictor
+ FP64 correction
1 GPU23.105 s17.520 s
2 GPUs12.387 s9.349 s
4 GPUs7.120 s5.605 s

6.40× throughputFour cards with FP32 share prediction + FP64 correction, relative to CPU64.

  • Same 256 economies, both periods and three moments.
  • Every result passes the final FP64 residual checks and matches the reference results.
  • All-FP32 economies were not measured.
What is included?
  • INTEL(R) XEON(R) GOLD 6530 + NVIDIA L40S, one machine.
  • The clock covers sending out and collecting the work, building the inputs, transfers, the whole solves and the results returned to the host, including the solver’s diagnostics and the transfers back from the GPU.
  • It excludes imports, starting the workers, setting up the draws, compilation, warm-up and the extra messages of the detailed traces.
  • All 5,376 timed results passed the checks.
  • FP32 is used only for share predictors. Correction, prices, roots, exit, fixed costs and moments use FP64.
  • More cards split independent candidate economies.
04 / Dependencies

What can run together?

Independent economies can overlap. Within an economy, later steps still need earlier answers. Move through this conceptual map to see the difference.

Four groups are shown. Each needs a free worker, not another row’s answer.

Can run nowWaiting
AParameters + draws
Solve economy A
Three moments
BParameters + draws
Solve economy B
Three moments
CParameters + draws
Solve economy C
Three moments
DParameters + draws
Solve economy D
Three moments
Then compare the candidate losses
Solve together
No cross-row dependency.

Common draws can be shared read-only. No economy needs another economy’s solved result.

Earlier experiments Single-thread CPU and share-block references
Earlier reference · one CPU thread

A different baseline.

8 identical economies · 3 repeats

13.43× against one native CPU thread in this earlier run.

Finish the same batch.

CPU · 1 thread47.93 s
GPU · whole solve3.57 s
Median wall time. Both transfers and all returned firm arrays included.
17 whole economies checked.Same float64 precision and original exit ruleThree moments, φ and operating firms agreeIdentical operating sets and exit pathsFirm and market arrays run on GPU. Input preparation and the scalar upper-root solver stay on CPU.

σ 0.047041 · N 14.2227 · ζ 0.919212 · seed 100

CPU and GPU are compared within each saved case. Candidate 1 uses a native 14-firm draw bank; Teaching baseline uses the first 14 rows of the 18-firm teaching bank. Both use NumPy PCG64 with seed 100, but the two banks give different draws, even where the rounded parameters and the seed match.

CPU and GPU outputs for one fixed economy
QuantityCPUGPU
Employment reallocation28.6378%28.6378%
Sales-weighted markup1.450131.45013
SGA / total cost20.4991%20.4991%
Fixed labor φ · per million2.06252.0625
Operating firms / market8.4618.461

All numerical checks passed · identical operating sets and exit pathsSaved outputs

NVIDIA L40S / INTEL(R) XEON(R) GOLD 6530 · one native CPU thread · common input draws prepared before the clock

All cases & numerical evidence
Earlier component test · shares only

One block of the solver.

14 firms × 10,000 markets

9.9–11.3× faster for this share block, including transfers.

All firms active

14 firms in every market.

CPU · 1 core189.8 ms
GPU · resident16.3 ms
GPU + transfers16.8 ms
11.3× with transfers · CPU is this card’s baseline

Mixed active

Empty, monopoly and oligopoly markets.

CPU · 1 core163.5 ms
GPU · resident15.9 ms
GPU + transfers16.5 ms
9.9× with transfers · CPU is this card’s baseline

Median of 5 timed solves after warmup · float64 · NVIDIA L40S / Intel Xeon Gold 6530

Shares only. The 256 full-economy CPU experiment below measures different work.
05 / Earlier CPU recordings

Replay the CPU worker runs

These runs were timed from the start of the worker pool to its shutdown, including creating the draws and writing the results. Each worker count was timed once, without a warm-up, so this clock differs from the warmed comparison in GPU measurements.

—

Move between five actual runs.

Wall time
—
Until the batch finishes
Speed-up
—
Relative to one worker
Efficiency
—
Speed-up per worker
Worker core-time
—
Workers × wall time

One lane per worker.

Each block is one economy.
Recorded execution intervals of the 256 economies, arranged by worker.
Each timeline uses its own time scale. Compare speed with the metrics above.
Time-compressed replayPaused at the end
Recorded time
—
Economies finished
— / 256
Workers busy
—

Loading the saved measurements…

Time saved, resources used

Five recorded CPU runs, one for each worker count.

Measured speed-up and efficiency across five worker counts, with a separately labelled ideal reference.
Points are measured; the ideal line is labelled.
Measured performance of the same batch at each worker count
WorkersWall timeSpeed-upEfficiencyWorker core-time

06 / Code and resources

Run the CPU and GPU code

Built from the course repository

The current source bundle contains the CPU model and tests, the batched GPU solver, optional fusion, mixed-precision and resident variants, and a single-economy GPU reference. Its lessons/04_parallel folder reruns this unit’s experiments: the CPU worker timings, the two short exercises and the GPU checks against the CPU solver.

Start with the README and python run.py --help inside the bundle. CPU: Python, NumPy and SciPy. GPU: a CUDA-capable PyTorch environment; fused variants also need Triton.

Try it yourself

Time the CPU workers on your computer.

Download the current source, unpack it, and run this command in the unpacked folder or at the root of the course repository. It needs Python 3.10 or later with NumPy and SciPy. On four cores it takes about four minutes.

python lessons/04_parallel/scaling.py --cases 16

The script solves the first 16 of the 256 timed economies with 1, 2, 4, … worker processes, up to the number of cores, each with one linear-algebra thread. It times each pool from start-up to shutdown and compares every result with unit 03’s saved values. Without --cases 16 it solves all 256, as in the earlier CPU recordings; one worker then takes about 20 minutes.

Check your result

Every line should end with “matches saved results: True”. The times depend on your computer; on the four-core computer used to test this page:

 1 workers:    128.4 s   matches saved results: True
 2 workers:     68.2 s   matches saved results: True
 4 workers:     37.7 s   matches saved results: True

Compare your speedups T(1)/T(W) with the earlier CPU recordings rather than with the worker count. Here they are 1.9 with two workers and 3.4 with four. The recordings of all 256 economies reached 2.02 and 4.01, because start-up takes a smaller share of a longer run.

On a computer with an NVIDIA GPU and PyTorch, python lessons/04_parallel/check_gpu.py checks the GPU code against the CPU solver; its last line should read “All GPU checks passed.”

What did the earlier CPU worker recordings measure?

The first 256 candidate economies for the year 2000, with 10,000 markets and two periods. Every run uses the same inputs, numerical precision and fixed-cost rule.

Wall time includes process-pool startup, draws, solving, result writes and shutdown. Waiting for the machine and setting it up are outside the clock. Each worker uses one numerical-library thread and a different physical core.

Each is a single timing on one shared machine, so the variation between repeated runs is unknown.

This is exercise 6 of unit 03. In the download, lessons/04_parallel/scaling.py runs it again on your machine.

Read the method note ↗