Re: [PATCH v4 2/3] system/memory: Use qemu_ram_move() for directly accessible regions

Gavin Shan <[email protected]> Tue, 28 Jul 2026 13:09:46 +1000
Newsgroups org.nongnu.qemu-arm,org.nongnu.qemu-devel
Message-ID <[email protected]>
On 7/27/26 11:52 PM, Philippe Mathieu-Daudé wrote:
> On 27/7/26 14:51, Peter Maydell wrote:
>> On Mon, 27 Jul 2026 at 04:27, Gavin Shan <[email protected]> wrote:
>>
>> Initial note: I think this is basically the right thing; I have
>> some suggestions for beefing up the doc comment and some minor
>> other things below.
>>
>>> All ram device regions were turned to be indirectly accessible by commit
>>> 4a2e242bbb ("memory: Don't use memcpy for ram_device regions"). This leads
>>> to guest hang on attempt to build 'cuda-samples' as reported by Julia. The
>>> guest is started by the following command lines, with GH100 GPU card passed
>>> from the host.
>>>
>>>     host$ lspci | grep GH100
>>>     0009:01:00.0 3D controller: NVIDIA Corporation GH100 [GH200 120GB / 480GB] (rev a1)
>>>     host$ /home/sandbox/gavin/qemu.main/build/qemu-system-aarch64            \
>>>           -machine virt,gic-version=host,ras=on,highmem-mmio-size=4T         \
>>>           -accel kvm -cpu host -smp cpus=48 -m size=8G                       \
>>>           -drive file=/home/gavin/sandbox/images/disk.qcow2,if=none,id=d0    \
>>>           -device virtio-blk-pci,id=vb0,bus=pcie.0,drive=d0,num-queues=4     \
>>>           -device vfio-pci-nohotplug,host=0009:01:00.0,bus=pcie.1.0
>>>             :
>>>     guest$ cd cuda-samples/build
>>>     guest$ make -j 20 clean
>>>     guest$ make -j 20
>>>             :
>>>     [ 54%] Linking CUDA executable graphMemoryNodes
>>>     [ 54%] Built target graphMemoryNodes
>>>     <no more output afterwards, guest becomes frozen here>
>>>
>>>     guest$ qemu-system-aarch64: virtio: bogus descriptor or out of resources
>>>     [  555.814025] virtio_blk virtio0: [vda] new size: 268435456 512-byte logical blocks (137 GB/128 GiB)
>>>
>>> When the GPU's driver (NVidia open driver) is loaded on guest bootup,
>>> the memory blocks residing in the PCI BAR#4 of the GH100 GPU card can
>>> be presented to the guest through memory hot-add. The page cache can
>>> then be allocated from the hot added memory blocks when cuda-samples
>>> is being built. Afterwards, the page cache is sent to QEMU's virtio-blk
>>> device as part of the DMA request, the bounce buffer has to be used to
>>> accomodate the request as the corresponding memory region (MemoryRegion)
>>> is an indirectly accessible ram device region in qemu. However, the max
>>> bounce bufer size is only 4096 bytes by default and that is exhausted
>>> quickly, leading to a reset on the virtio-blk device and frozen guest
>>> eventually.
>>>
>>>    QEMU
>>>    ====
>>>    virtio_blk_handle_output
>>>      virtio_blk_handle_vq
>>>        virtio_blk_get_request
>>>          virtqueue_pop
>>>            virtqueue_split_pop
>>>              virtqueue_map_desc
>>>                address_space_map
>>>                  memory_access_is_direct         # Return false
>>>                    memory_region_supports_direct_access
>>>
>>>    (qemu) info mtree
>>>    memory-region: pci_bridge_pci
>>>      0000000000000000-ffffffffffffffff (prio 0, container): pci_bridge_pci
>>>        0000042000000000-0000043fffffffff (prio 1, i/o): 0009:01:00.0 base BAR 4
>>>          0000042000000000-0000043fffffffff (prio 0, i/o): 0009:01:00.0 BAR 4
>>>            0000042000000000-000004379fffffff (prio 0, ramd): 0009:01:00.0 BAR 4 mmaps[0]
>>>
>>> This adds qemu_ram_move() where the aligned and small-sized accesses are
>>> handled by qatomics, and fall back to memmove() otherwise. The memove()
>>> for the directly accessible regions is replaced by qemu_ram_move() so that
>>> the issue covered by commit 4a2e242bbb (MMIO access instructions were
>>> optimized to SSE instructions) is fixed. This makes 'ram_device_mem_ops'
>>> redundant, paving the way to revert that commit to make the ram device
>>> region directly accessible again in the next patch.
>>
>> I think this commit message should also describe the second category
>> of bug that we intend it to fix: the one where a device does e.g.
>> address_space_stb() to a data structure in guest memory and requires
>> it to write exactly that byte exactly once.
>>
>>
>>> Reported-by: Julia Graham <[email protected]>
>>> Suggested-by: Michael S. Tsirkin <[email protected]>
>>> Suggested-by: Peter Xu <[email protected]>
>>> Suggested-by: Richard Henderson <[email protected]>
>>> Suggested-by: Peter Maydell <[email protected]>
>>> Signed-off-by: Gavin Shan <[email protected]>
> ---
> 
>>> +void qemu_ram_move(void *dst, const void *src, size_t n)
>>> +{
>>> +    uintptr_t test, len;
>>> +
>>> +    if (src == dst || n == 0) {
>>
>> I think we should not bother testing for src == dst. It's
>> vanishingly unlikely to actually happen, and if it does
>> happen then the code will work fine, and it's probably better
>> to actually do the access in that case than to skip it.
>>
>>> +        return;
>>> +    }
>>> +
>>> +    test = (uintptr_t)src | (uintptr_t)dst | n;
>>> +    len = test & -test;
>>
>> What is this doing? I am not a fan of clever bit twiddling
>> that isn't commented to explain itself. Readers of the code
>> shouldn't have to go off and search for an explanation of what
>> is going on.
> 
> "max alignment of the 3 values"?
> 

Yes, it's the maximal length of the aligned access that is determined
by @src, @dst and @n. I've put a comments for (v5).

     /*
      * Maximal length of aligned access that are determined by @src,
      * @dst and @n
      */

>>
>>> +
>>> +    /* Overlapping buffers, unaligned or oversized access */
>>> +    if (n > 8 || len != n) {
>>> +        memmove(dst, src, n);
>>> +        return;
>>> +    }
>>> +
>>> +    switch (len) {
>>> +    case 1:
>>> +        qatomic_set((uint8_t *)dst, qatomic_read((uint8_t *)src));
>>> +        break;
>>> +    case 2:
>>> +        qatomic_set((uint16_t *)dst, qatomic_read((uint16_t *)src));
>>> +        break;
>>> +    case 4:
>>> +        qatomic_set((uint32_t *)dst, qatomic_read((uint32_t *)src));
>>> +        break;
>>> +    case 8:
>>> +        qatomic_set((uint64_t *)dst, qatomic_read((uint64_t *)src));
>>> +        break;
>>> +    default:
>>> +        g_assert_not_reached();
>>> +    }
>>> +}
> 
> Maybe more readable (untested):
> 
> -- >8 --
>   void qemu_ram_move(void *dst, const void *src, size_t len)
>   {
>       if (unlikely(n == 0)) {
>           return;
>       }
>       if (len == 1) {
>           qatomic_set((uint8_t *)dst, qatomic_read((uint8_t *)src));
>           return;
>       } else if (QEMU_PTR_IS_ALIGNED(dst, len) && QEMU_PTR_IS_ALIGNED(src, len)) {
>           switch (len) {
>           case 2:
>               qatomic_set((uint16_t *)dst, qatomic_read((uint16_t *)src));
>               return;
>           case 4:
>               qatomic_set((uint32_t *)dst, qatomic_read((uint32_t *)src));
>             return;
>           case 8:
>               qatomic_set((uint64_t *)dst, qatomic_read((uint64_t *)src));
>               return;
>           default:
>               break;
>         }
>      }
>      /* Overlapping buffers, unaligned or oversized access */
>      memmove(dst, src, len);
>   }
> ---
> 

Thanks for the proposed code, but I'd like to keep what we already had. One thing
I try to avoid from the beginning is the unnecessary nested if statements. Besides,
one irrelevant question is that QEMU_PTR_IS_ALIGNED() looks a bit strange as it
uses % operator, meaning it works even the operand isn't power of 2.

#define QEMU_IS_ALIGNED(n, m) (((n) % (m)) == 0)
#define QEMU_PTR_IS_ALIGNED(p, n) QEMU_IS_ALIGNED((uintptr_t)(p), (n))

QEMU_PTR_IS_ALIGNED(3, 3) => true

Thanks,
Gavin