Multi-Stage Build Size Reduction
Problem Statement
A multi-stage build compiles wheels, CUDA extensions and datasets in a fat builder stage, then copies only the finished artefacts into a slim runtime stage. Everything else in the builder is discarded — it never reaches the registry. Compute how much that actually saves.
Background
The shipped image is the runtime stage: its base image, its own layers, plus whatever COPY --from=builder pulled across. The builder's compilers, caches and intermediate object files are left behind.
The single-stage baseline used here is the honest comparison: you would have done everything in one image on top of the builder's base, so you pay for every builder layer plus the runtime stage's non-base layers.
multi_stage_mb = sum(runtime_layers) + copied_mb
single_stage_mb = sum(builder_layers) + sum(runtime_layers[1:])
saved_mb = single_stage_mb - multi_stage_mb
reduction_pct = 100 * saved_mb / single_stage_mb
Note runtime_layers[1:] — the single-stage build does not carry a second base image.
Your Task
Implement:
def multi_stage_report(builder_layers, runtime_layers, copied_mb):
Return a dict with keys "multi_stage_mb", "single_stage_mb", "saved_mb" and "reduction_pct", in that order, each rounded to 2 decimal places.
Input Format
- builder_layers: list of numbers, MB added by each builder-stage layer (index 0 is the builder base image).
- runtime_layers: list of numbers, MB added by each runtime-stage layer (index 0 is the runtime base image).
- copied_mb: number, total MB copied from the builder into the runtime stage.
Output Format
- A dict with the four keys above, values rounded to 2 decimals.
Sample
print(multi_stage_report([900.0, 15.0, 640.0], [110.0, 4.0], 52.0))
Output:
{'multi_stage_mb': 166.0, 'single_stage_mb': 1559.0, 'saved_mb': 1393.0, 'reduction_pct': 89.35}
The runtime stage is 114 MB plus a 52 MB virtualenv copied from the builder, so 166 MB ships. The one-stage equivalent drags the 900 MB toolchain base and the 640 MB build cache along: 1559 MB.
Example:
print(multi_stage_report([900.0, 15.0, 640.0], [110.0, 4.0], 52.0))
{'multi_stage_mb': 166.0, 'single_stage_mb': 1559.0, 'saved_mb': 1393.0, 'reduction_pct': 89.35}Shipped = 110 + 4 + 52 = 166 MB. Single-stage = 900 + 15 + 640 (builder) + 4 (the runtime layer, minus its base image) = 1559 MB. Saved = 1393 MB, i.e. 1393 / 1559 = 89.35% smaller.
Constraints:
- 1 <= len(builder_layers) <= 50
- 1 <= len(runtime_layers) <= 50
- All sizes are non-negative numbers in MB
reduction_pctmay be negative when the copied artefacts are larger than what the builder stage saved- Round every returned value to 2 decimal places
1. Background Knowledge
Multi-stage builds are a fundamental optimization technique in containerization (e.g., Docker) used to minimize the final image size. In a typical ML or software deployment, the builder stage contains heavy dependencies like compilers, CUDA toolkits, and build caches required to compile code. The runtime stage only contains the lightweight dependencies needed to execute the compiled application. By separating these, the final image excludes the bulky build tools, significantly reducing storage costs and deployment time.
The problem asks you to quantify this benefit by comparing two scenarios:
- Multi-stage: The final image consists of the runtime base, runtime-specific layers, and only the compiled artifacts copied from the builder.
- Single-stage (Baseline): A hypothetical scenario where everything is built in one go. This includes the entire builder environment (base + all layers) plus the runtime-specific layers (excluding the runtime base, as it would be part of the single unified base).
Understanding the difference between layers and final image size is crucial. In Docker, an image is a stack of layers. The builder_layers list includes the base image at index 0. The runtime_layers list also includes its own base image at index 0. In a single-stage build, you don't have two separate base images; you have one unified base (the builder's base) and then all subsequent layers from both stages are stacked on top.
2. Algorithm Approach
The core approach is arithmetic aggregation and comparison. You need to calculate the total size of two distinct configurations based on the provided lists and a scalar value.
- Calculate Multi-Stage Size: Sum all elements in runtime_layers and add copied_mb. This represents the actual shipped image.
- Calculate Single-Stage Size: Sum all elements in builder_layers. Then, add the sum of runtime_layers excluding the first element (the runtime base image, which is redundant in a single-stage build because the builder base serves as the foundation).
- Compute Savings: Subtract the multi-stage size from the single-stage size.
- Compute Reduction Percentage: Divide the savings by the single-stage size and multiply by 100.
This is a straightforward data processing task requiring careful indexing and summation.
3. Step-by-Step Strategy
- Initialize Variables: Create variables to hold the calculated sizes for clarity.
- Compute multi_stage_mb:
- Use the sum() function on the runtime_layers list.
- Add the copied_mb value to this sum.
- Compute single_stage_mb:
- Sum all elements in builder_layers.
- Sum all elements in runtime_layers starting from index 1 (i.e., runtime_layers[1:]). This excludes the runtime base image.
- Add these two sums together.
- Compute saved_mb:
- Subtract multi_stage_mb from single_stage_mb.
- Compute reduction_pct:
- Calculate (saved_mb / single_stage_mb) * 100.
- Handle potential division by zero if single_stage_mb is 0 (though unlikely in real-world scenarios with valid inputs).
- Round Values:
- Round all four calculated values to 2 decimal places using Python's round(value, 2) function.
- Return Result:
- Construct and return a dictionary with the keys "multi_stage_mb", "single_stage_mb", "saved_mb", and "reduction_pct" in that specific order.
4. Common Pitfalls
- Including the Runtime Base in Single-Stage: The most common error is summing the entire runtime_layers list for the single-stage calculation. Remember, in a single-stage build, the builder's base image is the base. Adding the runtime base image again would double-count the foundational layer. Always use runtime_layers[1:].
- Order of Keys: The problem specifies the dictionary keys must be in a specific order: "multi_stage_mb", "single_stage_mb", "saved_mb", "reduction_pct". While Python 3.7+ preserves insertion order, ensure you construct the dict in this exact sequence to match expected outputs.
- Rounding Precision: Ensure you round to exactly 2 decimal places. Using round(x, 2) is standard, but be aware of floating-point arithmetic quirks. For example, round(89.355, 2) might behave unexpectedly due to binary representation, but for this problem, standard rounding is sufficient.
- Empty Lists: Although unlikely given the context, consider what happens if runtime_layers has only one element (the base). runtime_layers[1:] would be an empty list, and sum([]) is 0, which is correct.
5. Time & Space Complexity
- Time Complexity: O(N+M), where N is the number of elements in builder_layers and M is the number of elements in runtime_layers. This is because we iterate through each list once to compute the sums. The arithmetic operations are constant time O(1).
- Space Complexity: O(1). We only store a few numerical variables for the sums and the final dictionary. The slicing runtime_layers[1:] creates a new list in Python, which technically takes O(M) space, but this can be avoided by using sum(runtime_layers) - runtime_layers if memory optimization is critical. However, for typical input sizes, the space overhead is negligible.