Carla简单入门-2 同步,异步与交通管理器
本文写于2023年7月,文中所展示的版本为Ubuntu20.04以及Carla0.9.14,不同版本可能有一定的不同,欢迎各位伙伴们把遇到的问题和解决办法与其他人分享。
在上篇文档中,我们初步熟悉了如何使用PythonAPI去搭建一个基础的拟真交通环境以及实现一些我们自定义的需求。在最后我们遇到了主车的录像卡顿问题,这是由于Carla的同步异步设置导致的问题,这次我们就来了解一下同步与异步的区别和关系并且着手解决录像卡顿的问题。
1. 核心概念:同步与异步
同步与异步是极为重要的核心概念,对于同步与异步的正确设置与否将直接决定最终拟真时的结果的效率与正确率,官方文档中对此做了非常详尽的说明,英语好的伙伴们可以参阅官方文档: Synchrony and time-step - CARLA Simulator
a. 时间步长
- 首先要清楚的是,Carla模拟世界中的时间和现实世界中的时间是不同的,模拟世界有着自己的时钟和时间,由服务器控制,两个临近的模拟时刻之间模拟世界经过的时间我们称之为时间步长。服务器计算两个模拟时刻需要一些现实时间,计算两个模拟时刻所需的现实时间我们称之为现实时间步长。现实时间步长可能会与模拟时间步长时长不同。现实步长由服务器的性能以及场景的复杂程度决定,而模拟时间步长可以由使用者自行设定。服务器可能需要几毫秒来计算两个模拟场景,而这两个模拟场景之间可能模拟世界已经经过了1秒,也就是说,模拟时间步长为1s,我们可以根据我们的需求来设置为固定模拟时间步长或者可变模拟时间步长。
- 可变时间步长
CARLA的默认模式。模拟步骤之间经过的时间将是服务器计算所需时间。
settings = world.get_settings()
settings.fixed_delta_seconds = None # 设置可变时间步长
world.apply_settings(settings)
# 可以使用PythonAPI/util/config.py脚本来设置时间步长。将参数设置为零表示可变时间步长。
cd PythonAPI/util && python3 config.py --delta-seconds 0
- 固定时间步长
每个模拟步骤之间经过的时间保持恒定。如果将其设置为0.5秒,每秒将有两个模拟帧。使用相同的时间增量对于从模拟中获取数据是最好的方式。物理和传感器数据将对应于模拟中易于理解的时刻。此外,如果服务器足够快,可以在更少的实际时间内模拟更长的时间段。
可以在世界设置中设置固定的时间步长。要将模拟设置为固定时间步长为0.05秒,请应用以下设置。在这种情况下,模拟器将花费20个步骤(1/0.05)来重现模拟世界中的一秒。
settings = world.get_settings()
settings.fixed_delta_seconds = 0.05
world.apply_settings(settings)
# 也可以使用提供的脚本PythonAPI/util/config.py来设置固定时间步长。
cd PythonAPI/util && python3 config.py --delta-seconds 0.05
b. 模拟记录(recorder)
- Carla提供了recorder的特性,方便我们记录下模拟的状态并且重新进行回放模拟。需要注意的是,我们在不同的同步异步设定时回放模拟的精度可能会受到影响。
- 固定模拟时间步长( fixed time-step) 当我们使用固定时间步长设定时进行回放模拟将很容易,将服务器的时间步长设定被记录模拟的时间步长即可。
- 可变模拟时间步长( variable time-step) 当我们使用可变时间步长设定时,回放模拟就会变得复杂一些:
如果服务器使用可变时间步长运行,那么时间步长将与原始模拟不同,对于缺失的步长的数据将使用记录的数据进行插值,这可能会导致回放模拟结果与原始记录结果不同。
如果服务器被强制复现完全相同的时间步长,尽管回放模拟的时间步长是相同的,但是由于服务器的性能波动或者差异,真实世界时间将会不同,模拟将以奇怪的时间波动方式回放。
同时,使用可变时间步长也会导致 浮点数算术误差,因为时间是连续变量,每个时间步长中间对于时间的浮点误差累积可能会导致最终结果误差甚至错误,以至于无法重现回放模拟。
c. 物理子步 (Physics substepping)
- 为了精确计算物理效果,物理模拟必须在非常小的时间步内进行。当我们在模拟中每帧执行多次计算(例如传感器渲染,读取存储等)时,时间步长可能会成为一个问题。由于这个限制仅发生在物理模拟中,我们可以仅对物理计算应用子步。在默认情况下物理子步是打开的,并且被设定为每个时间步长最大10个物理子步没,每个物理子步最大为0.01秒。当然,我们可以通过API调整这些设定:
settings = world.get_settings() # 获取当前设定
settings.substepping = True #启用物理子步
settings.max_substep_delta_time = 0.01 # 将物理子步最大步长设定为0.01秒
settings.max_substeps = 10 # 设定每时间步长最多有10个物理子步
world.apply_settings(settings) # 应用设定
- 注意,当启用同步模式时,我们设定模拟时间步长时需要遵循以下条件来保证物理模拟的准确性:
fixed_delta_seconds <= max_substep_delta_time * max_substeps
- 注意,为了保证物理模拟的准确性,子步的时间间隔应该至少低于0.01666,理想情况下低于0.01。
- 下图展示了在固定模拟时间步长为0.04秒的模拟中,速度随物理时间步长的变化而变化。我们可以看到,一旦物理时间步长超过0.01,速度的稳定性开始出现偏差,并且随着物理时间步长的增加而加剧。
d. 客户端-服务器同步 (Client-server synchrony)
- CARLA采用客户端-服务器架构。服务器运行模拟,客户端获取信息并对世界进行修改。本节涉及客户端和服务器之间的通信。
默认情况下,CARLA以异步模式运行。服务器尽可能快地运行模拟,而不等待客户端。在同步模式下,服务器在更新到下一个模拟步骤之前会等待客户端发送的“ready to go”的消息。 - 设置同步模式
在同步和异步模式之间切换只需要改变 settings.synchronous_mode的值即可:
settings = world.get_settings()
settings.synchronous_mode = True # 启用同步模式
world.apply_settings(settings)
- 同时运行多个client时,只能有一个client开启同步模式,因为server会对每个收到的“ready to go”信息进行反应,多个client开启同步模式将会发送过多“ready to go”信息导致同步失败
- 注意,如果启用了同步模式,并且正在运行Traffic Manager,则Traffic Manager也必须设置为同步模式。
traffic_manager= client.get_trafficmanager() # 获得当前的traffic manager
traffic_manager.set_synchronous_mode(True) #将traffic manager设为同步模式
- 停用同步模式我们可以使用以下脚本,或者直接将变量设置为False即可:
cd PythonAPI/util && python3 config.py --no-sync # 禁用同步模式
e. 同步模式的使用
- 同步模式在客户端应用程序速度较慢以及需要传感器等不同元素之间的同步性时特别重要。如果客户端速度过慢,服务器不采用同步模式,信息将会溢出,导致信息丢失。例如当我们有多个传感器并且想把传感器画面存储到本地的情况,如果服务器不采用同步模式,那么我们传感器画面可能来不及存储就被覆盖,最后导致本地存储画面出现丢帧情况。
- 下面是一个在同步模式下读取传感器信息的代码片段:
settings = world.get_settings() # 获取当前模拟世界设定
settings.synchronous_mode = True # 将同步模式设定为打开
world.apply_settings(settings) # 应用设定
camera = world.spawn_actor(blueprint, transform) # 生成传感器
image_queue = queue.Queue()
camera.listen(image_queue.put) # 将传感器画面放入队列中
while True:
world.tick() # 更新模拟环境
image = image_queue.get() # 从队列中读取传感器画面
-
这段代码首先注意到的是
world.tick()
这个函数。它的作用是更新整个模拟世界。同时我们还会发现这里用了python自带的Queue, queue.get有一个功效,就是在它把列队里所有内容都提取出来之前,会阻止任何其他进程越过自己这一步,相当于一个blocker,在client读取完queue的数据之前,queue会将其他进程,例如更新整个模拟世界的进程阻挡住,这样我们就能做到client每读取一帧画面,server渲染下一帧。如果没有这个queue,我们会发现仿真虽然设置成了同步模式,但是server和client还是无法同步运行。
所以可以这样理解,
settings.synchronous_mode = True
让仿真的更新要通过这个client来唤醒,但这并不能保证它会等该client其他进程运行完,必须要再加一个queue来阻挡一下它,逼迫它等着该客户其他线程搞定。也就是说,启动同步模式,让你的server学会等待客户的必要条件有三个:
- settings.synchronous_mode = True
- world.tick()
- Thread Blocker(例如Queue)
-
来自基于GPU的传感器(主要是相机)的数据通常会有几帧的延迟。在这种情况下,同步性至关重要。
f. 同步与异步的适用范围
- 首先粘上官方文档的链接,里面有详细说明: Synchrony and time-step - CARLA Simulator
- 同步模式 + 可变时间步长: 几乎可以确定这是一种不可取的状态。当时间步长大于0.1秒时,物理模拟将无法正常运行。尤其是当client较慢时(例如添加了较多传感器),这种情况很有可能发生,模拟时间和物理将无法同步,模拟将失效。
- 异步模式 + 可变时间步长: 这是CARLA的默认状态。Server和client是异步的。模拟时间会自动根据现实时间调整。注意进行回放模拟时需要考虑浮点数算术误差和server之间可能存在的时间步长差异。
- 异步模式 + 固定时间步长: Server将尽可能快地运行。检索到的信息将与模拟中的模拟时间被一一对应起来,所以回放模拟时不会产生浮点数算术误差。如果服务器性能足够快,这种配置可以在较少的实际时间内模拟较长的模拟时间段。
- 同步模式 + 固定时间步长: Client将控制模拟进程。直到client发送信息后Server才会计算下一步。当同步性和精确性很重要时,这是最佳模式。特别是在client速度较慢或有多个传感器读取信息时。
2. 代码演示:同步与异步
对于同步与异步的演示,我会基于上篇文章 Carla简单入门-1 基本的API使用 中代码的基础上进行修改演示,感兴趣自己尝试的伙伴们可以在上篇文章文末找到完整代码。
a.首先是默认的异步模式 + 可变时间步长:
在这个模式下,我们并不需要对于模拟世界的设定或者traffic manager的设定做任何调整,我们只需要在进入while True循环之前设定好传感器,让它将读取到的数据存储到本地即可,代码如下:
# 设定读取的数据的存储路径
output_path = os.path.join("/home/ziyu/data/carla_pic", '%06d.png')
# 令摄像头读取数据并存储
camera.listen(lambda image: image.save_to_disk(output_path % image.frame))
下面是最终效果的参考图:
我们可以看到,在默认的异步模式 + 可变时间步长下,因为server无需等待client的进程,模拟进行的非常快,让我们可以在同样现实时间内进行更长模拟时间的模拟(对比同步模式gif中车辆速度)同时这也造成了传感器记录画面的丢帧,卡顿。