Skip to content

SWT-Bench cold image builds regress badly under shared registry cache export #510

Description

@simonrosenberg

Summary

This is the SWT-Bench-specific sub-issue for the broader image build regression tracked in #504.

After going through the historical SWT runs carefully, the picture is:

  1. The best historical cold SWT run we still have is much faster than current March runs.
  2. SWT was already slower in early March before the shared-cache SDK change landed.
  3. The latest and worst SWT collapse happens when the SDK starts exporting a shared registry cache from every parallel SWT build.
  4. SWT is especially sensitive because it fans out across hundreds of mostly unique base images, so the shared cache is cold at the start and many workers try to write to the same cache ref concurrently.

Cold vs warm classification

These runs should not be used to measure cold-build throughput:

  • 23000734281: fully warm, artifact manifest shows 433 skips / 0 builds.
  • 22989698608: fully warm, artifact manifest shows 433 skips / 0 builds.
  • 22985173172: fully warm, artifact manifest shows 433 skips / 0 builds.
  • 22967516108: mixed, artifact manifest shows 288 skips / 145 builds.

These are the most useful comparison points:

  • 21870877504 (2026-02-10, SDK cfe52af, best historical cold baseline available)
    • no already exists. Skipping build. lines in the run log
    • command used --max-workers 4
  • 23001334237 (2026-03-12, SDK aa9df69, near-cold partial run)
    • manifest mostly real builds before cancellation
    • command used --max-workers 4
  • 23008709458 (2026-03-12, SDK d0c1a39, near-cold partial run)
    • manifest shows only 12 skips in the first 170 completions
    • command used --max-workers 4
  • 23017371689 (2026-03-12, SDK fc962c4, latest bad run)
    • manifest shows 44 builds / 27 skips before cancellation
    • command used --max-workers 16 --build-batch-size 50

Throughput regression

Useful milestone timings from the raw GitHub logs:

Run SDK Classification 50/433 100/433 150/433
21870877504 cfe52af best cold baseline 18m29s 40m52s 55m41s
23001334237 aa9df69 near-cold 41m07s 1h20m24s -
23008709458 d0c1a39 near-cold / lightly mixed 48m18s 1h41m39s 2h30m46s
23017371689 fc962c4 mixed, latest bad run 2h00m52s - -

Equivalent completion throughput:

  • 21870877504: about 162 images/hour to 50, 147 images/hour to 100
  • 23001334237: about 73 images/hour to 50, 75 images/hour to 100
  • 23008709458: about 62 images/hour to 50, 59 images/hour to 100
  • 23017371689: about 25 images/hour to 50

So there are really two regressions:

  1. Feb cold baseline -> early March pre-shared-cache runs: roughly 2.0x to 2.6x slower.
  2. Early March pre-shared-cache runs -> fc962c4 shared-cache run: another roughly 2.4x to 3.0x slower by the 50-image mark, even though the run was configured with higher nominal concurrency.

Relative to the best cold baseline, the latest fc962c4 run is about 4.9x slower at the 50-image mark.

What changed in code

Between the good February baseline and early March

The early March runs (aa9df69 / d0c1a39) are still using the older SWT workflow shape with --max-workers 4.

Those runs do include SDK build-path changes relative to cfe52af, including a heavier builder environment and then the prebuilt-sdist optimization. The prebuilt-sdist optimization helps remove repeated uv build --sdist, but it does not explain the remaining SWT slowdown by itself.

Between d0c1a39 and fc962c4

There are two important changes in the latest bad regime:

  1. The SWT workflow was changed from default max-workers=4 to max-workers=16, and agent image submit batch size was raised to 50.
  2. The SDK build path changed from exporting one registry cache to exporting two caches for pushed builds:
    • the image-specific cache ref
    • a shared cache ref

In openhands/agent_server/docker/build.py, fc962c4 introduces shared_cache_tag / shared_cache_tags and then emits two --cache-to type=registry,... destinations for a pushed build.

That change is the key new behavior for the worst SWT run.

Why SWT is hit harder than SWE

SWT expands to a very large set of distinct base images. On the current dataset, collect_unique_base_images() yields 433 unique base images.

That means the first wave of a cold SWT run gets very little value from a shared cache that has not been seeded yet. At the same time, with the fc962c4 behavior, many workers are all trying to export to the same shared registry cache ref.

So SWT pays both sides of the penalty:

  • little cache reuse at the start, because the build graph is cold and the images are diverse
  • heavy contention and upload cost on cache-to because many workers write the same shared ref

Direct evidence from the fc962c4 run

The run-level progress already looks bad, but the per-image logs make the root cause much clearer.

Representative examples from 23017371689:

  • astropy-12907
    • image export itself took about 167s
    • the two registry cache exports then took about 870s and 874s
  • django-10554
    • image export itself took about 79s
    • the two registry cache exports then took about 2294s and 2301s

In other words, some individual SWT builds are spending 15 to 38 minutes after the image is already built, almost entirely in exporting cache to registry.

That strongly points at docker buildx --cache-to type=registry,... as the current dominant bottleneck.

Root-cause hypothesis

Current best explanation:

  1. There was already an earlier SWT slowdown between cfe52af and the early March SDKs (aa9df69 / d0c1a39). That older slowdown is likely due to SDK build-path changes that made the build heavier even before shared cache was introduced.
  2. The latest severe SWT collapse is caused by the shared registry cache export added in fc962c4.
  3. The workflow change to --max-workers 16 and --build-batch-size 50 likely amplifies the problem, because more cold SWT builds are now trying to export the same shared cache concurrently.
  4. The old repeated-sdist issue is no longer the primary SWT bottleneck in these latest runs, because the slow shared-cache runs are already reusing a prebuilt sdist.

Implications

For SWT specifically, the main problem now is not the Dockerfile's main build steps. It is the cache export strategy under cold, high-fanout image building.

If we want to recover SWT throughput, we likely need to change the cache strategy, not just keep optimizing sdist creation.

Likely next steps

  • Disable shared cache-to export for the normal parallel SWT worker pool.
  • If we still want a shared cache, seed it once in a controlled warm-up step instead of having every worker write it.
  • Re-test fc962c4 behavior at --max-workers 4 to separate pure shared-cache cost from the concurrency amplification.
  • Separately isolate the earlier Feb -> early March slowdown, which predates shared_cache_tag.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions