Series contents · Resources and Scheduling · 阅读中文版
“I only generated a slightly larger output, and the compiler says global memory is exhausted.”
“Does it know how much memory the runtime will provide?”
“It knows a default number.”
The danger is that such a default often survives long enough to look like a hardware fact. It may originally have served one deployment environment, then been copied into a compile-time ceiling imposed on every model.
Three Different Questions Squeezed into One Capacity
Address allocation faces at least three kinds of constraint. First, storage capacity: how many bytes the runtime actually allocates. Second, address format: how large an address instructions, descriptors, and runtime fields can represent. Third, the compiler’s own integer arithmetic: whether alignment, addition, and conversion can overflow.
These may happen to produce similar numbers, but they mean different things. If the compiler plans relative offsets while the loader determines final storage, putting a fixed physical capacity into the general global-address pool may reject artifacts that could otherwise be deployed.
Removing a capacity ceiling does not make addresses unlimited, either. An address may fit a wide host integer and still be unencodable in a narrower device-transfer descriptor.
The relevant commit explicitly decouples global-address planning from old physical-capacity metadata while retaining capacity constraints for finite local storage. A public explanation must preserve that distinction: this corrects the classification of constraints; it does not make the hardware’s memory larger.
A Teaching Example
Suppose the compiler plans an 11 KiB logical region, the runtime can provide 20 KiB, and the compiler retains an old 8 KiB default budget. Rejection under that budget mistakes a runtime resource condition for a fixed compile-time condition.
Now suppose a descriptor’s address format can represent values only up to M. Every allocation must still obey an explicit address-range contract. The reviewed implementation also requires the aligned exclusive end address to be representable, giving this teaching condition:
1 | start <= M |
The last address and the exclusive end address are different boundaries. If hardware needs to encode only the last valid byte, the contract may permit a different upper limit. If runtime metadata must also store the end offset, the exclusive end matters. This is an interface choice, not something to silently rewrite as start + size - 1 out of habit.
The reviewed tests specifically exercise sizes near the boundary, showing that this choice is intentional and tested in the source.
Why Subtract Before Comparing?
An intuitive implementation is:
1 | if start + size > limit: |
But if addition overflows first and produces a small number, the comparison may incorrectly pass. A safer structure checks start first, then asks whether size exceeds the remaining space.
Alignment has the same issue. In the familiar expression (value + alignment - 1) / alignment * alignment, the first addition can overflow near the integer limit. The verified change introduces checked alignment: reject zero alignment, calculate the required padding, and check whether value can accept that padding before adding it.
Using redesigned small numbers, let the alignment unit be 10 and the current value 27. Padding is 3 and the result is 30. If the maximum representable value is 29, reject before addition rather than compute 30 and narrow it afterward. The example deliberately avoids a power of two: not every alignment interface can be assumed to use a bit mask.
One Rule Must Cover Every Allocation Path
If the main address allocator removes the old budget but code generation retains a default capacity, a model passes early and fails at the end. If ordinary tensor allocation checks address width but tiled planning bypasses it, a large graph may still produce unencodable descriptors.
The reviewed change covers the main allocator, tiled planning, and additional storage allocated during generation, using a common address-range check. Generation overhead may include descriptors or instruction data. “The tensors fit” does not imply that everything allocated afterward will fit too.
It also removes old capacity-summary fields so downstream stages do not continue interpreting obsolete metadata as a limit. This is a general migration problem: removing old logic does not prevent another stage from reviving it through leftover metadata. Data-model changes require checking producers, consumers, and serialized information together.
A Pool Without a Capacity Limit Still Has Limits
A planning pool without a fixed physical capacity must still check at least four things: whether the raw size is negative, whether alignment is valid, whether address-plus-size overflows, and whether the result fits the final encoding. An external base address adds another check: base plus relative offset.
The reviewed code also guards against negative values and overflowing sums in existing external-memory summaries. The reason is practical. If an earlier stage records a corrupt peak, subsequent allocation must not interpret that corruption as permission to continue from a small offset.
These compile-time checks still do not establish that the runtime successfully allocated physical memory. A useful product interface should distinguish an unrepresentable address, overflow in planning arithmetic, and insufficient deployment capacity so users can respond appropriately.
Are There Performance Benefits?
Removing an incorrect compile-time capacity barrier allows valid sizes through planning. It does not guarantee faster execution. Larger models may even have worse memory-access or cache behavior.
Checked integer arithmetic usually adds a small amount of compiler work, but the actual cost depends on allocation frequency and data structures. More useful questions include whether unified rules eliminate duplicate checks, reject invalid models earlier, or avoid wasting time before a late serialization error.
Peak metrics also need precise names. Global logical high-water mark, peak simultaneously live local storage, artifact overhead, and runtime reservation are not one number. Labeling all of them “memory usage” makes performance analysis harder to diagnose than the original failure.
Can the Allocator Still Be Used After an Error?
Once capacity categories are clear, inspect failure-state behavior. Allocation commonly involves alignment, choosing a free segment, advancing the heap top, recording a mapping, and updating the peak. If address-format validation happens after some state has changed, returning failure does not necessarily leave the allocator in its pre-request state.
That may be acceptable when any failure aborts compilation. A search algorithm that wants to try another layout, however, needs rollback, tentative allocation, or a temporary context. Two callers cannot silently assign different guarantees to the same interface returning false.
Diagnostics should also distinguish requested size from aligned reservation. A teaching request for 21 bytes may reserve 30 after alignment. If an error prints only the request, a user may reasonably wonder why 25 apparently free bytes are insufficient. Showing the start, requested size, reserved size, and source of the limit makes the result something that can be checked by hand.
Likewise, a high logical peak does not necessarily mean much data is live at once. Sparse address layouts, external reservations, and appended descriptors can all raise the maximum end address. Inspect both the interval set and the high-water mark rather than inferring a large, optimizable leak from one peak number.
Regression Matrix
| Boundary | Expected behavior | Reason |
|---|---|---|
| Above the old default budget but still encodable | Planning can continue | A fixed physical capacity is no longer misapplied |
| Close to the address upper bound | Follow the explicit end-address contract | Avoid off-by-one errors |
| Out of range after alignment | Reject | A valid raw size may become invalid after alignment |
| Valid start, enormous length | Reject without overflowing the addition | Prevent wraparound |
| Negative size or corrupt existing summary | Reject | Do not turn an error into a valid offset |
| Tensors fit, appended artifacts do not | Reject during generation | Later allocations obey the same constraints |
The commit’s tests cover allocation beyond the old budget, invalid ranges, and further growth during generation. Only their contents were verified; no execution results were obtained.
A separate teaching program could use arbitrary-precision integers as a reference and enumerate start, size, and alignment for small address widths. This exhaustively exercises boundaries without allocating large memory regions, allowing the checked implementation to be compared with the reference case by case. The target is arithmetic and the contract, not proving correctness by exhausting the machine.
The next time a compiler says “out of memory,” first ask which kind of shortage it means. Correct limits protect programs. Conflated limits merely make an error message sound certain.
Back to series contents · Previous · Next
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !