| # Memory Model Relaxation Annotations | |
| ## Introduction | |
| Memory Model Relaxation Annotations (MMRAs) are target-defined properties | |
| on instructions that can be used to selectively relax constraints placed | |
| by the memory model. For example: | |
| - The use of `VulkanMemoryModel` in a SPIRV program allows certain | |
| memory operations to be reordered across `acquire` or `release` | |
| operations. | |
| - OpenCL APIs expose primitives to only fence a specific set of address | |
| spaces. Carrying that information to the backend can enable the | |
| use of faster synchronization instructions, rather than fencing all | |
| address spaces everytime. | |
| MMRAs offer an opt-in system for targets to relax the default LLVM | |
| memory model. | |
| As such, they are attached to an operation using LLVM metadata which | |
| can always be dropped without affecting correctness. | |
| ## Definitions | |
| memory operation | |
| : A load, a store, an atomic, or a function call that is marked as | |
| accessing memory. | |
| synchronizing operation | |
| : An instruction that synchronizes memory with other threads (e.g. | |
| an atomic or a fence). | |
| tag | |
| : Metadata attached to a memory or synchronizing operation | |
| that represents some target-defined property regarding memory | |
| synchronization. | |
| An operation may have multiple tags that each represent a different | |
| property. | |
| A tag is composed of a pair of metadata string: a *prefix* and a *suffix*. | |
| In LLVM IR, the pair is represented using a metadata tuple. | |
| In other cases (comments, documentation, etc.), we may use the | |
| `prefix:suffix` notation. | |
| For example: | |
| :::{code-block} | |
| :caption: 'Example: Tags in Metadata' | |
| !0 = !{!"scope", !"workgroup"} # scope:workgroup | |
| !1 = !{!"scope", !"device"} # scope:device | |
| !2 = !{!"scope", !"system"} # scope:system | |
| ::: | |
| :::{note} | |
| The only semantics relevant to the optimizer is the | |
| "compatibility" relation defined below. All other | |
| semantics are target defined. | |
| ::: | |
| Tags can also be organised in lists to allow operations | |
| to specify all of the tags they belong to. Such a list | |
| is referred to as a "set of tags". | |
| :::{code-block} | |
| :caption: 'Example: Set of Tags in Metadata' | |
| !0 = !{!"scope", !"workgroup"} | |
| !1 = !{!"sync-as", !"private"} | |
| !2 = !{!0, !2} | |
| ::: | |
| :::{note} | |
| If an operation does not have MMRA metadata, it's treated as if | |
| it has an empty list (`!{}`) of tags. | |
| ::: | |
| Note that it is not an error if a tag is not recognized by the | |
| instruction it is applied to, or by the current target. | |
| Such tags are simply ignored. | |
| Both synchronizing operations and memory operations can have | |
| zero or more tags attached to them using the `!mmra` syntax. | |
| For the sake of readability in examples below, | |
| we use a (non-functional) short syntax to represent MMMRA metadata: | |
| :::{code-block} | |
| :caption: Short Syntax Example | |
| store %ptr1 # foo:bar | |
| store %ptr1 !mmra !{!"foo", !"bar"} | |
| ::: | |
| These two notations can be used in this document and are strictly | |
| equivalent. However, only the second version is functional. | |
| compatibility | |
| : Two sets of tags are said to be *compatible* iff, for every unique | |
| tag prefix P present in at least one set: | |
| - the other set contains no tag with prefix P, or | |
| - at least one tag with prefix P is common to both sets. | |
| The above definition implies that an empty set is always compatible | |
| with any other set. This is an important property as it ensures that | |
| if a transform drops the metadata on an operation, it can never affect | |
| correctness. In other words, the memory model cannot be relaxed further | |
| by deleting metadata from instructions. | |
| (HappensBefore)= | |
| ## The *happens-before* Relation | |
| Compatibility checks can be used to opt out of the *happens-before* relation | |
| established between two instructions. | |
| Ordering | |
| : When two instructions' metadata are not compatible, any program order | |
| between them are not in *happens-before*. | |
| For example, consider two tags `foo:bar` and | |
| `foo:baz` exposed by a target: | |
| ``` | |
| A: store %ptr1 # foo:bar | |
| B: store %ptr2 # foo:baz | |
| X: store atomic release %ptr3 # foo:bar | |
| ``` | |
| In the above figure, `A` is compatible with `X`, and hence `A` | |
| happens-before `X`. But `B` is not compatible with | |
| `X`, and hence it is not happens-before `X`. | |
| Synchronization | |
| : If an synchronizing operation has one or more tags, then whether it | |
| synchronizes-with and participates in the `seq_cst` order with | |
| other operations is target dependent. | |
| Whether the following example synchronizes with another sequence depends | |
| on the target-defined semantics of `foo:bar` and `foo:bux`. | |
| ``` | |
| fence release # foo:bar | |
| store atomic %ptr1 # foo:bux | |
| ``` | |
| ### Examples | |
| Example 1: | |
| : ``` | |
| A: store ptr addrspace(1) %ptr2 # sync-as:1 vulkan:nonprivate | |
| B: store atomic release ptr addrspace(1) %ptr3 # sync-as:0 vulkan:nonprivate | |
| ``` | |
| A and B are not ordered relative to each other | |
| (no *happens-before*) because their sets of tags are not compatible. | |
| Note that the `sync-as` value does not have to match the `addrspace` value. | |
| e.g. In Example 1, a store-release to a location in `addrspace(1)` wants to | |
| only synchronize with operations happening in `addrspace(0)`. | |
| Example 2: | |
| : ``` | |
| A: store ptr addrspace(1) %ptr2 # sync-as:1 vulkan:nonprivate | |
| B: store atomic release ptr addrspace(1) %ptr3 # sync-as:1 vulkan:nonprivate | |
| ``` | |
| The ordering of A and B is unaffected because their set of tags are | |
| compatible. | |
| Note that A and B may or may not be in *happens-before* due to other reasons. | |
| Example 3: | |
| : ``` | |
| A: store ptr addrspace(1) %ptr2 # sync-as:1 vulkan:nonprivate | |
| B: store atomic release ptr addrspace(1) %ptr3 # vulkan:nonprivate | |
| ``` | |
| The ordering of A and B is unaffected because their set of tags are | |
| compatible. | |
| Example 4: | |
| : ``` | |
| A: store ptr addrspace(1) %ptr2 # sync-as:1 | |
| B: store atomic release ptr addrspace(1) %ptr3 # sync-as:2 | |
| ``` | |
| A and B do not have to be ordered relative to each other | |
| (no *happens-before*) because their sets of tags are not compatible. | |
| ## Use-cases | |
| ### SPIRV `NonPrivatePointer` | |
| MMRAs can support the SPIRV capability | |
| `VulkanMemoryModel`, where synchronizing operations only affect | |
| memory operations that specify `NonPrivatePointer` semantics. | |
| The example below is generated from a SPIRV program using the | |
| following recipe: | |
| - Add `vulkan:nonprivate` to every synchronizing operation. | |
| - Add `vulkan:nonprivate` to every non-atomic memory operation | |
| that is marked `NonPrivatePointer`. | |
| - Add `vulkan:private` to tags of every non-atomic memory operation | |
| that is not marked `NonPrivatePointer`. | |
| ``` | |
| Thread T1: | |
| A: store %ptr1 # vulkan:nonprivate | |
| B: store %ptr2 # vulkan:private | |
| X: store atomic release %ptr3 # vulkan:nonprivate | |
| Thread T2: | |
| Y: load atomic acquire %ptr3 # vulkan:nonprivate | |
| C: load %ptr2 # vulkan:private | |
| D: load %ptr1 # vulkan:nonprivate | |
| ``` | |
| Compatibility ensures that operation `A` is ordered | |
| relative to `X` while operation `D` is ordered relative to `Y`. | |
| If `X` synchronizes with `Y`, then `A` happens-before `D`. | |
| No such relation can be inferred about operations `B` and `C`. | |
| :::{note} | |
| The [Vulkan Memory Model](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-model-non-private) | |
| considers all atomic operation non-private. | |
| Whether `vulkan:nonprivate` would be specified on atomic operations is | |
| an implementation detail, as an atomic operation is always `nonprivate`. | |
| The implementation may choose to be explicit and emit IR with | |
| `vulkan:nonprivate` on every atomic operation, or it could choose to | |
| only emit `vulkan::private` and assume `vulkan:nonprivate` | |
| by default. | |
| ::: | |
| Operations marked with `vulkan:private` effectively opt out of the | |
| happens-before order in a SPIRV program since they are incompatible | |
| with every synchronizing operation. Note that SPIRV operations that | |
| are not marked `NonPrivatePointer` are not entirely private to the | |
| thread --- they are implicitly synchronized at the start or end of a | |
| thread by the Vulkan *system-synchronizes-with* relationship. This | |
| example assumes that the target-defined semantics of | |
| `vulkan:private` correctly implements this property. | |
| This scheme is general enough to express the interoperability of SPIRV | |
| programs with other environments. | |
| ``` | |
| Thread T1: | |
| A: store %ptr1 # vulkan:nonprivate | |
| X: store atomic release %ptr2 # vulkan:nonprivate | |
| Thread T2: | |
| Y: load atomic acquire %ptr2 # foo:bar | |
| B: load %ptr1 | |
| ``` | |
| In the above example, thread `T1` originates from a SPIRV program | |
| while thread `T2` originates from a non-SPIRV program. Whether `X` | |
| can synchronize with `Y` is target defined. If `X` synchronizes | |
| with `Y`, then `A` happens before `B` (because A/X and | |
| Y/B are compatible). | |
| #### Implementation Example | |
| Consider the implementation of SPIRV `NonPrivatePointer` on a target | |
| where all memory operations are cached, and the entire cache is | |
| flushed or invalidated at a `release` or `acquire` respectively. A | |
| possible scheme is that when translating a SPIRV program, memory | |
| operations marked `NonPrivatePointer` should not be cached, and the | |
| cache contents should not be touched during an `acquire` and | |
| `release` operation. | |
| This could be implemented using the tags that share the `vulkan:` prefix, | |
| as follows: | |
| - For memory operations: | |
| - Operations with `vulkan:nonprivate` should bypass the cache. | |
| - Operations with `vulkan:private` should be cached. | |
| - Operations that specify neither or both should conservatively | |
| bypass the cache to ensure correctness. | |
| - For synchronizing operations: | |
| - Operations with `vulkan:nonprivate` should not flush or | |
| invalidate the cache. | |
| - Operations with `vulkan:private` should flush or invalidate the cache. | |
| - Operations that specify neither or both should conservatively | |
| flush or invalidate the cache to ensure correctness. | |
| :::{note} | |
| In such an implementation, dropping the metadata on an operation, while | |
| not affecting correctness, may have big performance implications. | |
| e.g. an operation bypasses the cache when it shouldn't. | |
| ::: | |
| ### Memory Types | |
| MMRAs may express the selective synchronization of | |
| different memory types. | |
| As an example, a target may expose an `sync-as:<N>` tag to | |
| pass information about which address spaces are synchronized by the | |
| execution of a synchronizing operation. | |
| :::{note} | |
| Address spaces are used here as a common example, but this concept | |
| can apply for other "memory types". What "memory types" means here is | |
| up to the target. | |
| ::: | |
| ``` | |
| # let 1 = global address space | |
| # let 3 = local address space | |
| Thread T1: | |
| A: store %ptr1 # sync-as:1 | |
| B: store %ptr2 # sync-as:3 | |
| X: store atomic release ptr addrspace(0) %ptr3 # sync-as:3 | |
| Thread T2: | |
| Y: load atomic acquire ptr addrspace(0) %ptr3 # sync-as:3 | |
| C: load %ptr2 # sync-as:3 | |
| D: load %ptr1 # sync-as:1 | |
| ``` | |
| In the above figure, `X` and `Y` are atomic operations on a | |
| location in the `global` address space. If `X` synchronizes with | |
| `Y`, then `B` happens-before `C` in the `local` address | |
| space. But no such statement can be made about operations `A` and | |
| `D`, although they are performed on a location in the `global` | |
| address space. | |
| #### Implementation Example: Adding Address Space Information to Fences | |
| Languages such as OpenCL C provide fence operations such as | |
| `atomic_work_item_fence` that can take an explicit address | |
| space to fence. | |
| By default, LLVM has no means to carry that information in the IR, so | |
| the information is lost during lowering to LLVM IR. This means that | |
| targets such as AMDGPU have to conservatively emit instructions to | |
| fence all address spaces in all cases, which can have a noticeable | |
| performance impact in high-performance applications. | |
| MMRAs may be used to preserve that information at the IR level, all the | |
| way through code generation. For example, a fence that only affects the | |
| global address space `addrspace(1)` may be lowered as | |
| ``` | |
| fence release # sync-as:1 | |
| ``` | |
| and the target may use the presence of `sync-as:1` to infer that it | |
| must only emit instruction to fence the global address space. | |
| Note that as MMRAs are opt in, a fence that does not have MMRA metadata | |
| could still be lowered conservatively, so this optimization would only | |
| apply if the front-end emits the MMRA metadata on the fence instructions. | |
| ## Additional Topics | |
| :::{note} | |
| The following sections are informational. | |
| ::: | |
| ### Performance Impact | |
| MMRAs are a way to capture optimization opportunities in the program. | |
| But when an operation mentions no tags or conflicting tags, | |
| the target may need to produce conservative code to ensure correctness | |
| at the cost of performance. This can happen in the following situations: | |
| 1. When a target first introduces MMRAs, the | |
| frontend might not have been updated to emit them. | |
| 2. An optimization may drop MMRA metadata. | |
| 3. An optimization may add arbitrary tags to an operation. | |
| Note that targets can always choose to ignore (or even drop) MMRAs | |
| and revert to the default behavior/codegen heuristics without | |
| affecting correctness. | |
| ### Consequences of the Absence of *happens-before* | |
| In the {ref}`happens-before<HappensBefore>` section, we defined how an | |
| *happens-before* relation between two instruction can be broken | |
| by leveraging compatibility between MMRAs. When the instructions | |
| are incompatible and there is no *happens-before* relation, we say | |
| that the instructions "do not have to be ordered relative to each | |
| other". | |
| "Ordering" in this context is a very broad term which covers both | |
| static and runtime aspects. | |
| When there is no ordering constraint, we *could* statically reorder | |
| the instructions in an optimizer transform if the reordering does | |
| not break other constraints as single location coherence. | |
| Static reordering is one consequence of breaking *happens-before*, | |
| but is not the most interesting one. | |
| Run-time consequences are more interesting. When there is a | |
| *happens-before* relation between instructions, the target has to emit | |
| synchronization code to ensure other threads will observe the effects of | |
| the instructions in the right order. | |
| For instance, the target may have to wait for previous loads & stores to | |
| finish before starting a fence-release, or there may be a need to flush a | |
| memory cache before executing the next instruction. | |
| In the absence of *happens-before*, there is no such requirement and | |
| no waiting or flushing is required. This may noticeably speed up | |
| execution in some cases. | |
| ### Combining Operations | |
| If a pass can combine multiple memory or synchronizing operations | |
| into one, it needs to be able to combine MMRAs. One possible way to | |
| achieve this is by doing a prefix-wise union of the tag sets. | |
| Let A and B be two tags set, and U be the prefix-wise union of A and B. | |
| For every unique tag prefix P present in A or B: | |
| - If either A or B has no tags with prefix P, no tags with prefix | |
| P are added to U. | |
| - If both A and B have at least one tag with prefix P, all tags with prefix | |
| P from both sets are added to U. | |
| Passes should avoid aggressively combining MMRAs, as this can result | |
| in significant loss of information. While this cannot affect | |
| correctness, it may affect performance. | |
| As a general rule of thumb, common passes such as SimplifyCFG that | |
| aggressively combine/reorder operations should only combine | |
| instructions that have identical sets of tags. | |
| Passes that combine less frequently, or that are well aware of the cost | |
| of combining the MMRAs can use the prefix-wise union described above. | |
| Examples: | |
| ``` | |
| A: store release %ptr1 # foo:x, foo:y, bar:x | |
| B: store release %ptr2 # foo:x, bar:y | |
| # Unique prefixes P = [foo, bar] | |
| # "foo:x" is common to A and B so it's added to U. | |
| # "bar:x" != "bar:y" so it's not added to U. | |
| U: store release %ptr3 # foo:x | |
| ``` | |
| ``` | |
| A: store release %ptr1 # foo:x, foo:y | |
| B: store release %ptr2 # foo:x, bux:y | |
| # Unique prefixes P = [foo, bux] | |
| # "foo:x" is common to A and B so it's added to U. | |
| # No tags have the prefix "bux" in A. | |
| U: store release %ptr3 # foo:x | |
| ``` | |
| ``` | |
| A: store release %ptr1 | |
| B: store release %ptr2 # foo:x, bar:y | |
| # Unique prefixes P = [foo, bar] | |
| # No tags with "foo" or "bar" in A, so no tags added. | |
| U: store release %ptr3 | |
| ``` |