| .. _amdgpu-memmodel: | |
| ===================== | |
| AMDGPU Memory Model | |
| ===================== | |
| .. contents:: | |
| :local: | |
| Introduction | |
| ============ | |
| The :ref:`LLVM memory model<memmodel>` provides broad guarantees that are | |
| sufficient to implement inter-thread communication via memory. But in most | |
| communication patterns, not all memory accesses performed by a thread need to be | |
| exposed to other threads. Even when they do need to be exposed, not all threads | |
| may need to observe these memory accesses. This document describes the *AMDGPU | |
| memory model* that allows the user to control how the side-effects of memory | |
| accesses are propagated across threads. The programmer expresses this using | |
| **new intrinsics and metadata** as described below, and the implementation can | |
| then choose a more efficient mechanism to complete them, such as the cache | |
| policy bits in an AMDGPU device. | |
| The AMDGPU memory model allows executions that are not allowed by the LLVM | |
| memory model. At the same time, a simple mapping can be used to implement these | |
| new intrinsics and metadata using operations defined in the default LLVM memory | |
| model. Thus, **there exists a safe-by-default implementation** that produces | |
| executions that are valid in both models. | |
| Terminology | |
| =========== | |
| Memory Accesses | |
| Operations that read or write locations in memory are termed as *memory | |
| accesses*. Typical examples are ``load``, ``store`` and atomic instructions, | |
| as well as many intrinsics. | |
| Synchronizing Operations | |
| Synchronizing operations control how the side-effects of memory accesses are | |
| propagated in the system. Typical examples are atomic operations (including | |
| fences) with at least ``release`` or ``acquire`` ordering. | |
| .. _amdgpu-scopes: | |
| Scopes | |
| ====== | |
| A *scope* is an abstract description of sets of memory accesses and | |
| synchronizing operations in a multi-threaded execution environment. Each such | |
| set is called an *instance* of that scope, or a *scope instance* for short. | |
| - Each memory access or synchronizing operation belongs to at most one | |
| instance of every scope defined by the target. | |
| - When an operation ``X`` specifies a scope ``S``, it indicates the instance of | |
| ``S`` that contains ``X``. This scope instance is also termed as *X's instance | |
| of scope S*, or just *X's scope instance* when ``S`` is implied by the | |
| context. | |
| - When an operation does not specify a scope, it indicates the *system* | |
| scope defined below. | |
| LLVM scopes | |
| ----------- | |
| The LLVM Language Reference defines the following :ref:`scopes<syncscope>`: | |
| *system scope* (empty string "") | |
| There exists a single instance of this scope that contains the memory accesses | |
| and synchronizing operations performed by all threads. | |
| "singlethread" scope | |
| Each thread corresponds to a "singlethread" scope instance that contains the | |
| memory accesses and synchronizing operations performed by that thread. | |
| AMDGPU scopes | |
| ------------- | |
| The AMDGPU backend further refines the LLVM scopes with the following | |
| target-defined scopes and constraints: | |
| - *system scope* (same as LLVM) | |
| - "agent" scope | |
| - "cluster" scope | |
| - "workgroup" scope | |
| - "wavefront" scope | |
| - "singlethread" scope (same as LLVM) | |
| These are arranged from largest scope (*system scope*) to smallest scope | |
| ("singlethread"). | |
| - Every instance ``X`` of some scope ``S1`` other than "singlethread" scope is | |
| partitioned by the scope ``S2`` one level below it. Each subset defined by this | |
| partition is an instance of ``S2`` and is called a *subscope instance* of ``X``. | |
| - It follows that if two scope instances ``X`` and ``Y`` intersect, then their | |
| intersection is the smaller of ``X`` and ``Y``. | |
| - A scope ``S1`` is a *subscope* of a scope ``S2`` if every instance of ``S1`` | |
| is a subscope instance of some instance of ``S2``. | |
| **Inclusive Scopes**: Two operations ``X`` and ``Y`` are said to have *inclusive | |
| scopes* if the scope instance of each operation contains the other operation. In | |
| that case, the *common scope instance* ``S'`` of ``X`` and ``Y`` is the | |
| intersection of their scope instances. The scope corresponding to ``S'`` is also | |
| termed as the *common scope* of ``X`` and ``Y``. | |
| Availability and Visibility | |
| =========================== | |
| The AMDGPU memory model is built on top of the :ref:`happens-before<memmodel>` | |
| order defined by the LLVM memory model. But when one of the new intrinsics or | |
| metadata is used, **happens-before by itself is not sufficient** to describe its | |
| observable effects. Instead, the AMDGPU model uses *availability* and | |
| *visibility* to describe how the side-effects of these operations propagate to | |
| other threads. | |
| Availability determines how *far* the side-effects of a write have been | |
| forwarded in the system relative to that write. Visibility determines how | |
| *close* the side-effects of the same write have reached relative to an observer | |
| operation (typically a read). | |
| The AMDGPU memory model *does not change the structure of happens-before*, but | |
| changes the rules that determine how operations may observe the side-effects of | |
| other operations that *happen-before* them. | |
| Consider a write ``W`` that ``happens-before`` a read ``R`` to the same address: | |
| - ``R`` can potentially observe the side-effects of ``W`` **only if W is | |
| visible** to ``R``. | |
| - ``W`` can potentially be visible to ``R`` **only if W is first made | |
| available** to ``R``. | |
| The instructions used in the default LLVM memory model automatically satisfy | |
| these necessary conditions, and hence they can be explained using the rules from | |
| either memory model. But the new intrinsics and metadata *opt out* of the LLVM | |
| memory model, and can only be explained using the AMDGPU memory model. | |
| .. _amdgpu-store-available: | |
| store-available | |
| --------------- | |
| .. code-block:: llvm | |
| @llvm.amdgcn.av.global.store.b128(ptr, value, scope) | |
| store atomic [syncscope("<target-scope>")] | |
| atomicrmw [syncscope("<target-scope>")] | |
| cmpxchg [syncscope("<target-scope>")] | |
| The ``@llvm.amdgcn.av.global.store.b128`` intrinsic performs a non-atomic | |
| *store-available* operation on ``ptr`` with scope ``scope``. | |
| An atomic operation that results in a store operation is a *store-available* | |
| operation with scope ``syncscope``. | |
| .. _amdgpu-load-visible: | |
| load-visible | |
| ------------ | |
| .. code-block:: llvm | |
| @llvm.amdgcn.av.global.load.b128(ptr, scope) | |
| load atomic [syncscope("<target-scope>")] | |
| atomicrmw [syncscope("<target-scope>")] | |
| cmpxchg [syncscope("<target-scope>")] | |
| The ``@llvm.amdgcn.av.global.load.b128`` intrinsic performs a non-atomic | |
| *load-visible* operation on ``ptr`` with scope ``scope``. | |
| An atomic operation that results in a read operation is a *load-visible* | |
| operation with scope ``syncscope``. | |
| .. note:: | |
| Metadata cannot be used to model this using ordinary load/store operations, | |
| because the scope is necessary for correctness. In a hypothetical operation | |
| like this: | |
| .. code-block:: llvm | |
| store ptr, data, !mmra !{!"amdgcn-av", !"workgroup"} | |
| If the metadata is dropped or ignored, there is no guarantee that the store | |
| will become available at the intended scope. In implementation terms, the | |
| store may be completed at a nearer cache than the one required for that | |
| scope. A corresponding *load-visible* that does not access the same near | |
| cache will fail to observe this store. | |
| .. _amdgpu-av-metadata: | |
| AV Metadata | |
| ----------- | |
| .. code-block:: llvm | |
| !mmra !{!"amdgcn-av", !"none"} | |
| The presence of this metadata removes the ability of synchronizing operations to | |
| establish availability and visibility, and essentially creates *non-av* synchronizing | |
| operations. | |
| For a synchronizing operation which itself accesses memory (e.g., ``store atomic | |
| release`` or ``load atomic acquire``), the metadata does not affect the | |
| availability or the visibility of the access performed by the operation itself. | |
| It only affects the synchronization of other memory accesses. | |
| MakeAvailable and MakeVisible | |
| ----------------------------- | |
| .. code-block:: llvm | |
| store atomic [syncscope("<target-scope>")] <ordering> [, !mmra !{!"amdgcn-av", !"none"}] | |
| load atomic [syncscope("<target-scope>")] <ordering> [, !mmra !{!"amdgcn-av", !"none"}] | |
| atomicrmw [syncscope("<target-scope>")] <ordering> [, !mmra !{!"amdgcn-av", !"none"}] | |
| cmpxchg [syncscope("<target-scope>")] <ordering> [, !mmra !{!"amdgcn-av", !"none"}] | |
| fence [syncscope("<target-scope>")] <ordering> [, !mmra !{!"amdgcn-av", !"none"}] | |
| A synchronizing operation with at least ``release`` ordering is a | |
| ``MakeAvailable`` operation with scope ``syncscope``, if it is not marked as | |
| ``!{!"amdgcn-av", !"none"}``. | |
| A synchronizing operation with at least ``acquire`` ordering is a | |
| ``MakeVisible`` operation with scope ``syncscope``, if it is not marked as | |
| ``!{!"amdgcn-av", !"none"}``. | |
| .. code-block:: llvm | |
| ; This includes the following operations: | |
| ; - The atomic store at "agent" scope, | |
| ; - A store-available operation at "agent" scope on `ptr`, | |
| ; - A `MakeAvailable` operation at "agent" scope that affects previous memory accesses. | |
| store atomic syncscope("agent") release ptr | |
| ; This includes the following operations: | |
| ; - The atomic store at "agent" scope, | |
| ; - A store-available operation at "agent" scope on `ptr`. | |
| ; Notably, it does not include a `MakeAvailable` operation on other memory accesses. | |
| store atomic syncscope("agent") release ptr, !mmra !{!"amdgcn-av", !"none"} | |
| Ordering | |
| ======== | |
| .. note:: | |
| **TODO:** These ordering operations affect all address spaces. We need to | |
| eventually make that a parameter similar to the storage class parameter on | |
| operations and orders in Vulkan. | |
| Availability Operation | |
| ---------------------- | |
| An operation ``X`` is an *availability operation* on a write ``W`` if one of the | |
| following holds: | |
| - ``X`` is ``W`` itself, and ``W`` is a *store-available* operation, or, | |
| - ``X`` is a ``MakeAvailable`` operation that follows ``W`` in program order, | |
| or, | |
| - ``X`` is a ``MakeAvailable`` operation whose scope instance includes ``W``, | |
| and there is an availability operation ``Z`` on ``W`` such that: | |
| - ``Z`` happens-before ``X``, and, | |
| - ``Z``'s scope instance includes ``X``. | |
| Then ``X`` makes ``W`` available in its own scope instance ``S`` and every | |
| subscope instance of ``S`` that also includes ``W``. | |
| Visibility Operation | |
| -------------------- | |
| An operation ``Y`` is a *visibility operation* on a write ``W`` if ``Y`` is a | |
| *load-visible* operation to the same address, or a ``MakeVisible`` operation, | |
| and one of the following holds: | |
| - There exists an *availability* operation ``X`` on write ``W`` such that: | |
| - ``X`` happens-before ``Y``, and, | |
| - ``X`` and ``Y`` specify inclusive scopes. | |
| Then ``Y`` makes ``W`` visible in the common scope instance ``S`` of ``X`` and | |
| ``Y``, and every subscope instance of ``S`` that includes ``Y``. | |
| - There exists a *visibility* operation ``X`` on write ``W`` such that: | |
| - ``X`` happens-before ``Y``, and, | |
| - ``X`` makes ``W`` visible in a scope instance ``S1`` that includes ``Y``, and, | |
| - ``X`` is included in the scope instance ``S2`` of ``Y``. | |
| Then ``Y`` makes ``W`` visible in the intersection ``S`` of ``S1`` and ``S2``, | |
| and every subscope instance of ``S`` that includes ``Y``. | |
| Location Order | |
| -------------- | |
| A write ``W`` is *location-ordered* before an access ``Y`` to the same address | |
| if ``W`` is program-ordered before ``Y``. | |
| A write ``W`` is *location-ordered* before a write ``W1`` to the same address if | |
| there exists an availability operation ``Z`` on ``W`` such that: | |
| - ``Z`` happens-before ``W1``, and, | |
| - ``W1`` is included in ``Z``'s scope instance. | |
| A write ``W`` is *location-ordered* before a read ``R`` to the same address if | |
| there exists a visibility operation ``Z`` on write ``W`` such that: | |
| - ``Z`` is ``R`` itself, or, | |
| - ``Z`` precedes ``R`` in program order. | |
| The AMDGPU memory model overrides the definition of each byte in the | |
| :ref:`LLVM memory model<memmodel>` as follows. | |
| Every (defined) read operation ``R`` reads a series of bytes written by | |
| (defined) write operations. Each initialized global is assumed to have an | |
| initial *system scoped* atomic write operation that is *location-ordered* before | |
| any other read or write to that same location. | |
| For each byte of a read ``R``, ``R`` may see any write to the same byte, except: | |
| - If a write ``W1`` is *location-ordered* before a write ``W2``, and ``W2`` is | |
| *location-ordered* before a read ``R``, then ``R`` may not see ``W1``. | |
| - If a read ``R`` happens-before a write ``W3``, then ``R`` may not see ``W3``. | |
| The value returned by ``R`` is then defined as follows: | |
| - If no write is *location-ordered* before a read ``R``, then ``R`` returns | |
| ``undef``. | |
| - Otherwise if the set consisting of ``R`` and all writes that ``R`` may see | |
| contains only atomic operations with inclusive scopes, then ``R`` returns the | |
| value written by one of those writes. | |
| - Otherwise, if ``R`` may see some write that is not *location-ordered* before | |
| ``R``, then ``R`` returns ``undef``. | |
| - Otherwise, if ``R`` may see exactly one write ``W``, then ``R`` returns the | |
| value written by ``W``. | |
| - Otherwise, ``R`` returns ``undef``. | |
| Properties | |
| ========== | |
| .. tip:: | |
| This section is informational. | |
| The following properties follow from the definitions above: | |
| 1. **Happens-before is necessary for location-order.** A write ``W`` is | |
| *location-ordered* before a read ``R`` only if ``W`` happens-before ``R``. | |
| This follows from the definition of availability and visibility operations, | |
| which always require a happens-before link with the preceding operation in | |
| the chain. | |
| 2. **A write cannot be made available in a scope that does not contain it.** The | |
| definition of an availability operation ``X`` requires that ``X``'s scope | |
| instance includes ``W`` as a precondition. Since every scope instance that | |
| includes ``X`` also includes ``W``, availability cannot reach a scope | |
| instance that excludes ``W``. In other words, availability can only "expand | |
| outwards" into progressively larger scopes. | |
| 3. **Visibility is bounded by availability.** When a write is available in a | |
| scope instance, it can be made visible in that scope instance by a visibility | |
| operation with the corresponding scope. Subsequent ``MakeVisible`` operations | |
| make that write visible into narrower scope instances towards the observer. | |
| 4. **A write can be made visible in a scope instance that does not contain it.** | |
| The definition of a *visibility operation* anchors scope instances to the | |
| observer (``Y``), not to the original write. The only precondition is that the | |
| write must already be visible or available in the scope instance of the | |
| visibility operation. | |
| 5. **Availability and visibility chains.** For a write ``W`` to be visible to a | |
| read ``R`` anywhere in the system, the sufficient condition is a chain of | |
| happens-before edges that include availability and visibility operations with | |
| inclusive scopes. It is not necessary that ``W`` and ``R`` themselves have | |
| inclusive scopes. Each link in the availability and visibility definitions | |
| only checks the immediate predecessor, so intermediate operations can bridge | |
| scope gaps that the endpoints cannot satisfy directly. Such a chain passes | |
| through at least one availability operation and at least one visibility | |
| operation with inclusive scopes, such that their common scope includes both | |
| ``W`` and ``R``. | |
| .. _amdgcn-av-vulkan: | |
| The Vulkan Memory Model | |
| ======================= | |
| The AMDGPU memory model draws heavily on the Vulkan memory model. In | |
| particular, the following instructions are equivalent. | |
| .. csv-table:: | |
| :header: "LLVM", "SPIRV", "Available/Visible Semantics" | |
| :widths: 20, 20, 60 | |
| "``load``", "``OpLoad NonPrivatePointer``", "\-" | |
| "``load-visible``", "``OpLoad NonPrivatePointer``", "``MakePointerVisible``" | |
| "``store``", "``OpStore NonPrivatePointer``", "\-" | |
| "``store-available``", "``OpStore NonPrivatePointer``", "``MakePointerAvailable``" | |
| "``load atomic``", "``OpAtomicLoad``", "``MakePointerVisible``. Also ``MakeVisible`` when order is at least ``acquire``." | |
| "``load atomic !{!""amdgcn-av"", !""none""}``", "``OpAtomicLoad``", "``MakePointerVisible``" | |
| "``store atomic``", "``OpAtomicStore``", "``MakePointerAvailable``. Also ``MakeAvailable`` when order is at least ``release``." | |
| "``store atomic !{!""amdgcn-av"", !""none""}``", "``OpAtomicStore``", "``MakePointerAvailable``" | |
| "``fence``", "``OpMemoryBarrier``", "``MakeAvailable`` when order is at least ``release``, and ``MakeVisible`` when order is at least ``acquire``." | |
| "``fence !{!""amdgcn-av"", !""none""}``", "``OpMemoryBarrier``", "\-" | |
| .. note:: | |
| The above table is representative only, and does not aim to be exhaustive. In | |
| particular, it does not list composite atomic operations like ``rmw`` and | |
| ``cmpxchg``. The ordering and semantics of these operations can be determined | |
| by combining suitable rules such as: | |
| - "``MakeAvailable`` if the order is at least ``release``, and the operation | |
| results in a store", | |
| - "Only if it is not marked as ``!{!"amdgcn-av", !"none"}``", etc. | |
| The AMDGPU memory model is a special case of the Vulkan memory model: | |
| a. LLVM fence/atomic ordering operations have ``MakeAvailable`` / | |
| ``MakeVisible`` semantics by default, thus satisfying the availability and | |
| visibility chains required in Vulkan. Hence the LLVM memory model is a | |
| "strong" subset of the Vulkan memory model. | |
| b. The AMDGPU memory model described here makes it possible to opt-out of the | |
| default ``MakeAvailable`` and ``MakeVisible`` semantics, and instead specify | |
| it on select places including the new *load-visible* and *store-available* | |
| operations. This expands the subset of the Vulkan memory model that can now | |
| be expressed in LLVM IR. |