You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CUDA.jl:如何优化CuArrays数据拷贝的时间效率?

优化CUDA.jl中受限n体问题的位置存储性能瓶颈

问题背景

我正在用Julia求解受限n体问题(引力场已通过解析法求解),场景为包含太阳、木星及大量小行星/火箭的2D太阳系。借助CUDA.jl的高阶抽象,已实现小行星传播过程的并行化,但每时间步后将所有小行星X、Y位置存入二维数组(每行对应一个时间步的位置)的数据拷贝与存储操作,成为程序性能瓶颈。

当前瓶颈代码

目前采用先将CuArray转换为普通Julia数组,再逐元素写入二维数组的方式:

particleXsRAM=Array(particleXs)
particleYsRAM=Array(particleYs)
for count in 1:numberParticles
    particleXHistories[step,count]=particleXsRAM[count]
    particleYHistories[step,count]=particleYsRAM[count]
end

已尝试的无效方案

  • 创建二维CuArray逐元素拷贝:实际由CPU执行,无性能提升
  • 创建CuArray数组:后续绘图耗时过高,性价比低

当前代码虽比非并行版本有性能提升,但未达预期,希望通过CUDA.jl的高阶抽象并行化该存储过程以优化性能。

完整代码示例

function MassCoordsCircle(rotation::AbstractFloat,radius::AbstractFloat)
    return radius*cos(rotation),radius*sin(rotation)

end

function FindForcesArr(rocketCoord::AbstractArray,mass1Coord::AbstractFloat,mass2Coord::AbstractFloat,rocketOtherCoord::AbstractArray,mass1OtherCoord::AbstractFloat,mass2OtherCoord::AbstractFloat,mass1::AbstractFloat,mass2::AbstractFloat,G::AbstractFloat)
    return -G*mass1*(rocketCoord.-mass1Coord) ./ sqrt.((rocketCoord.-mass1Coord) .^ 2 .+ (rocketOtherCoord.-mass1OtherCoord) .^2 ) .^ 3 .- G*mass2*(rocketCoord.-mass2Coord) ./ sqrt.((rocketCoord.-mass2Coord) .^ 2 .+ (rocketOtherCoord.-mass2OtherCoord) .^ 2) .^3

end

function TaylorArr(x::AbstractArray,y::AbstractArray,vx::AbstractArray,vy::AbstractArray,mass1::AbstractFloat,mass2::AbstractFloat,dMass1::AbstractFloat,dMass2::AbstractFloat,G::AbstractFloat,mass1Rotation::AbstractFloat,mass2Rotation::AbstractFloat,period::AbstractFloat,dt::AbstractFloat)
    mass1x,mass1y=MassCoords(mass1Rotation,dMass1)
    mass2x,mass2y=MassCoords(mass2Rotation,dMass2)
    ax=FindForcesArr(x,mass1x,mass2x,y,mass1y,mass2y,mass1,mass2,G)
    ay=FindForcesArr(y,mass1y,mass2y,x,mass1x,mass2x,mass1,mass2,G)
    return x .+ dt .* vx .+ dt^2/2 .* ax,y .+ dt.*vy .+ dt^2/2 .* ay,vx .+ dt .* ax,vy .+ dt .* ay
    
end

using CUDA
using Plots
using PyPlot
using PyCall
np=pyimport("numpy")

G=6.6726e-11
#Mass of Sun
mass1=1.989e30
#Mass of Jupiter
mass2=1.898e27
#Sun-Jupiter distance
d_mass1_mass2=7.7834082e11
timeStep=100000.0
numberParticles=2000

d_COM_mass1=d_mass1_mass2*mass2/(mass1+mass2)
d_COM_mass2=d_mass1_mass2*mass1/(mass1+mass2)
MassCoords=MassCoordsCircle
PropagateParticle=TaylorArr

period=sqrt(4*pi^2/(G*(mass1+mass2))*d_mass1_mass2^3)


steps=trunc(Int64,div(period,timeStep)+1)
mass1xHistory=zeros(steps)
mass1yHistory=zeros(steps)
mass2xHistory=zeros(steps)
mass2yHistory=zeros(steps)

particleXs=CUDA.randn(Float64,numberParticles).*1e12
particleYs=CUDA.randn(Float64,numberParticles).*1e12
particleVXs=CUDA.randn(Float64,numberParticles).*15000
particleVYs=CUDA.randn(Float64,numberParticles).*15000

particleXHistories=Array{Float64}(undef,steps,numberParticles)
particleYHistories=Array{Float64}(undef,steps,numberParticles)

time=0
step=1
while step<=steps
    #Sun is 180 degrees out of sync with Jupiter.
    mass1Rotation=time/period*2*pi+pi
    mass2Rotation=time/period*2*pi
    mass1x,mass1y=MassCoords(mass1Rotation,d_COM_mass1)
    mass2x,mass2y=MassCoords(mass2Rotation,d_COM_mass2)

    mass1xHistory[step]=mass1x
    mass1yHistory[step]=mass1y
    mass2xHistory[step]=mass2x
    mass2yHistory[step]=mass2y

    particleXsRAM=Array(particleXs)
    particleYsRAM=Array(particleYs)
    for count in 1:numberParticles
        particleXHistories[step,count]=particleXsRAM[count]
        particleYHistories[step,count]=particleYsRAM[count]

    end

    particleXs,particleYs,particleVXs,particleVYs=PropagateParticle(particleXs,particleYs,particleVXs,particleVYs,mass1,mass2,d_COM_mass1,d_COM_mass2,G,mass1Rotation,mass2Rotation,period,timeStep)

    time=time+timeStep
    step=step+1
        
end

plt.figure(1,figsize=[7,7])
plt.plot(mass1xHistory,mass1yHistory)
plt.plot(mass2xHistory,mass2yHistory)
plt.plot(np.array(particleXHistories),np.array(particleYHistories))
plt.xlim([-9e11,9e11])
plt.ylim([-9e11,9e11])
plt.show()

注:小行星初始位置与速度随机生成,选用Matplotlib而非Plots是因其绘图速度更快。

优化方案

1. 利用广播机制替代手动循环

直接将CuArray的内容拷贝到CPU数组的对应行,避免逐元素循环的开销:

# 替换原循环代码
particleXHistories[step, :] .= Array(particleXs)
particleYHistories[step, :] .= Array(particleYs)

Julia的广播机制会优化赋值操作,比手动循环效率更高,同时减少代码冗余。

2. 使用copyto!直接拷贝(推荐)

通过copyto!函数结合view创建的行视图,直接将GPU数据拷贝到CPU数组的指定位置,避免中间数组的创建:

# 替换原循环代码
copyto!(view(particleXHistories, step, :), particleXs)
copyto!(view(particleYHistories, step, :), particleYs)

这种方式减少了一次内存分配(无需创建particleXsRAM中间数组),直接完成设备到主机的内存拷贝,性能提升更明显。

3. 延迟拷贝(显存充足时适用)

如果GPU显存足够存储所有时间步的历史数据,可以先在GPU上存储历史,模拟完成后一次性拷贝到CPU:

# 初始化GPU上的历史数组(行=时间步,列=粒子)
particleXHistoriesGPU = CuArray{Float64}(undef, steps, numberParticles)
particleYHistoriesGPU = CuArray{Float64}(undef, steps, numberParticles)

# 模拟过程中直接写入GPU数组
particleXHistoriesGPU[step, :] .= particleXs
particleYHistoriesGPU[step, :] .= particleYs

# 模拟结束后一次性拷贝到CPU
particleXHistories = Array(particleXHistoriesGPU)
particleYHistories = Array(particleYHistoriesGPU)

此方法避免了每步的CPU-GPU数据交互,适合粒子数和时间步规模适中的场景;若显存不足,可分批次拷贝(如每100步拷贝一次到CPU)。

内容的提问来源于stack exchange,提问作者TimTheEnchanter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 16:25:25