分布式框架Ray——基本反模式

大家使用分布式框架Ray时,基本反模式就是大家容易范的错误;
本文在讲解基本反模式的同时,也给出了对应的解决方案。

1. 访问任务/参与者中的全局变量

不要修改远程函数中的全局变量,相反,将全局变量转换为参与者(actor)的局部实例变量。
由@ray.remote装饰的Ray任务和actor在不同的进程中运行,它们不与Ray驱动程序共享相同的地址空间;
也就是说,如果你定义了一个全局变量并改变了驱动程序中的值,这些改变不会反映在worker(也就是task和actor)中。

import ray
global_v = 3
ray.init()
@ray.remote
class A :
    def f (self):
        return global_v + 3
actor = A.remote()
global_v = 4
print(ray.get(actor.f.remote()))#output:6

注:这是因为在驱动程序中global_v的值变化没有反映给参与者,因为它们运行在不同的进程中。

import ray
global_v = 3
ray.init()
@ray.remote
class A :
    def f (self):
        return global_v + 3
global_v = 4
actor = A.remote()
print(ray.get(actor.f.remote()))#output:7

注:在调用之前修改全局变量的值是可以的

更好的方法: 使用参与者的实例变量(局部变量)来保存被多个工作者(任务和参与者)修改/访问的全局变量。

import ray
@ray.remote
class GlobalVarActor :
    def __init__ (self):
        self.global_v = 3
    # 可以修改global_v
    def set_global_v (self, v):
        self.global_v = v
    # 读取 global_v
    def get_global_v (self, v):
        return self.global_v
@ray.remote
class A :
    def __init__ (self, global_v_registry):
        self.global_v_registry = global_v_registry
    # 子类可以调用基类的函数
    def f (self):
        ray.get(self.global_v_registry.get_global_v.remote()) + 3
global_v_registry = GlobalVarActor.remote()
actor = A.remote(global_v_registry)
# 实际上也是在调用之前修改值
ray.get(global_v_registry.set_global_v( 4 ))
print(ray.get(actor.f.remote()))#output:7

2. 太细粒度的任务

避免过度并行,并行或分散任务通常比普通的函数有更高的开销。 因此,如果并行化一个执行非常快的函数,其开销可能比实际的函数调用花费的时间还要长! 为了处理这个问题,我们应该小心过多的并行化。如果你有一个功能或任务太小,你可以使用一种称为批处理的技术,让你的任务在单个任务中做更有意义的工作。

细粒度的任务(运行时间很短的任务) 使用ray反而得不偿失。

import ray
import time
ray.init()

@ray.remote
def double (number):
    return number * 2
numbers = list(range( 10000 ))
doubled_numbers = []
s=time.time()
for i in numbers:
    doubled_numbers.append(ray.get(double.remote(i)))
print( "ray并行计算: " + str(time.time() - s))

对细粒度的任务进行批处理,即每次执行细粒度大的任务(多个细粒度的任务), 使用ray。

@ray.remote
def double_list (list_of_numbers):
    return [number * 2 for number in list_of_numbers]
numbers = list(range( 10000 ))
doubled_list_refs = []
BATCH_SIZE = 100
s=time.time()
for i in range( 0 , len(numbers), BATCH_SIZE):
    # 批处理
    batch = numbers[i : i + BATCH_SIZE]
    doubled_list_refs.append(double_list.remote(batch))
doubled_numbers = []
for ref in doubled_list_refs:
    doubled_numbers.extend(ray.get(ref))
print( "批处理+ray并行计算: " + str(time.time() - s))

3. 在任务中不必要地调用ray.get

避免在中间步骤中过于频繁地调用ray.get(),直接使用对象引用,并且只在最后调用ray.get()来获得最终结果。
当ray.get()被调用时,对象必须被转移到调用ray.get()的worker/node。如果你不需要在任务中操作对象,你可能不需要对它调用ray.get() !
通常,最好的做法是在调用ray.get()之前等待尽可能长的时间,或者甚至设计你的程序来避免过早地调用ray.get()。

注意,在第一个示例中,我们调用ray.get(),这将迫使我们将大展示转移到驱动程序,然后再转移到reducer。
在固定版本中,我们只将对象的引用传递给reducer。reducer自动调用ray.get()一次,这意味着数据直接从generate_rollout传递到reducer,避免了驱动程序。

请始终记住这ray.get()是一个阻塞操作,因此如果急切地调用它会损害并行性。相反,您应该尝试编写ray.get()尽可能晚地调用的程序。

import ray
import time
import numpy as np
ray.init()
@ray.remote
def generate_rollout (n):
    return np.ones(( n,n ))
@ray.remote
def reduce (data):
    return np.sum(data)
    
s=time.time()
rollout = ray.get(generate_rollout.remote(1000))
reduced = ray.get(reduce.remote(rollout))
print( "2次ray.get: " + str(time.time() - s))

s=time.time()
rollout = generate_rollout.remote(1000)
reduced = ray.get(reduce.remote(rollout))
print( "1次ray.get: " + str(time.time() - s))

4. 大型/不可序列化对象的闭包捕获

在ray.remote函数或类中使用大型对象时要小心。
在定义ray.remote函数或类时,很容易在函数定义中隐式地捕获较大(超过几个MB)的对象。当试图定义函数时,这可能会导致低性能或内存异常,因为Ray不是设计来处理非常大的序列化函数或类的。

对于大型对象,有以下解决方案:

  • 使用ray.put() 将对象放入Ray对象存储,然后使用ray.get() 在任务中获得对象的视图(改进方案1);个人理解:大型的对象先放入put()获得并行化的对象ID(分割了很多),再将并行后的(小型的对象)放入ray函数中。
  • 通过传递lambda方法在任务中创建对象,而不是在驱动程序脚本中创建对象(改进方案2)不是特别懂。
  • 第二种方法是不可序列化对象的唯一选项。
import ray
import time
import numpy as np

ray.init() 
# 创建一个78 MB的数组,验证方法:sys.getsizeof(big_array)/1024/1024=78
s=time.time()
@ray.remote
def f1 ():
    return len(big_array) 
big_array = np.zeros( 100*100*1024 )
ray.get(f1.remote())
print( "原始方案: " + str(time.time() - s))

#=============== 改进方案1:=============
s=time.time()
@ray.remote
def f2 ():
    return len(ray.get(big_array)) 
big_array = ray.put(np.zeros( 100*100*1024 ))
ray.get(f2.remote())
print( "改进方案1: " + str(time.time() - s))

#=============== 改进方案2:=============
s=time.time()
array_creator = lambda : np.zeros( 100*100*1024 )
@ray.remote
def f3 ():
    array = array_creator()
    return len(array)
ray.get(f3.remote())
print( "改进方案2: " + str(time.time() - s))

5. 在循环中调用ray.get

如果在循环中调用ray.get(),循环将不会继续运行,直到对ray.get()的调用被解析,才会接着循环。
当在调度远程工作之后立即调用ray.get()时,循环会阻塞,直到获取对象后接着循环,只能以顺序处理
我们应该首先安排所有远程调用,然后并行处理这些调用。 在安排工作之后,我们可以一次请求所有的结果。

解决方案:将对ray.get()的调用与对远程函数的调用分开。
这样,所有远程进程都在我们等待结果之前生成,并可以在后台并行运行。此外,你可以传递一个对象引用列表给ray.get(),而不是一个接一个地调用它来等待所有的任务完成。

import ray
import time

ray.init()
@ray.remote
def f(n): 
    return n

returns1 = [] 
s=time.time()
# 在循环中使用ray.get, 解析完才接着下一步循环,浪费时间
for i in range(100): 
    returns1.append(ray.get(f.remote(i)))
print( "循环中使用ray.get: " + str(time.time() - s))
print(returns1)

s=time.time()
refs=[]
# 改进方案:循环生成任务,最后获取结果
for i in range(100): 
    refs.append(f.remote(i))
returns2 = ray.get(refs)
print( "改进方案: " + str(time.time() - s))
print(returns2)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值