orrb/remote_renderer.py (264 lines of code) (raw):
import grpc
import logging
import os
import platform
import socket
import subprocess
import time
import google.protobuf.json_format as json_format
import numpy as np
from copy import deepcopy
from queue import Queue
from orrb.queue_executor import QueueExecutorABC, QueueWorkerABC
import orrb.protos.RendererConfig_pb2 as renderer_config_pb2
import orrb.protos.RenderService_pb2 as render_service_pb2
import orrb.protos.RenderService_pb2_grpc as render_service_pb2_grpc
from orrb.utils import (
read_depth_image,
read_normals_image,
read_rgba_image,
read_segmentation_image,
)
# Uncompressed batches of images tend to be big, default for grpc is just 4MB.
DEFAULT_GRPC_MESSAGE_SIZE = 256 * 1024 * 1024
def _get_server_bind_host():
if platform.system() == 'Linux':
return socket.gethostname()
else:
return 'localhost'
def _create_render_service_stub(port, message_size=DEFAULT_GRPC_MESSAGE_SIZE, timeout=10.0):
channel = grpc.insecure_channel('%s:%d' % (_get_server_bind_host(), port),
[('grpc.max_receive_message_length', message_size)])
grpc.channel_ready_future(channel).result(timeout=timeout)
return render_service_pb2_grpc.RenderServiceStub(channel)
def _build_render_batch_request(workload, config):
request = render_service_pb2.RenderBatchRequest()
request.width = config.image_width
request.height = config.image_height
if 'seed' in workload:
request.batch_seed = workload['seed'] % (1 << 31) # Truncate to fit int32
use_entry_seeds = False
if 'seeds' in workload:
seeds = workload['seeds']
use_entry_seeds = True
request.use_entry_seeds = use_entry_seeds
request.render_alpha = config.render_alpha
request.render_depth = config.render_depth
request.render_normals = config.render_normals
request.render_segmentation = config.render_segmentation
for i, qpos in enumerate(workload['qpos']):
entry = request.entries.add()
entry.qpos[:] = qpos
if use_entry_seeds:
entry.seed = seeds[i]
for camera_name in config.camera_names:
request.camera_names.append(camera_name)
return request, len(workload['qpos'])
def _add_auxiliary_stream(batch_dataset, batch_size, stream, dtype):
data = np.array(stream.data, dtype=dtype)
data_length = len(data)
if data_length % batch_size == 0:
data = data.reshape((batch_size, len(data) // batch_size))
batch_dataset[stream.name] = data
else:
logging.warning(f'Error in "{stream.name}" len: {len(data)} batch: {batch_size}.')
def _convert_render_batch_response(response, config, batch_size):
h, w = config.image_height, config.image_width
batch_dataset = dict()
for stream in response.streams:
stream_rgb_dataset = []
stream_depth_dataset = []
stream_normals_dataset = []
stream_segmentation_dataset = []
for entry in stream.entries:
if entry.image_data:
image = read_rgba_image(entry.image_data, w, h)
stream_rgb_dataset.append(image)
if entry.depth_data:
image = read_depth_image(entry.depth_data, w, h)
stream_depth_dataset.append(image)
if entry.normals_data:
image = read_normals_image(entry.normals_data, w, h)
stream_normals_dataset.append(image)
if entry.segmentation_data:
image = read_segmentation_image(entry.segmentation_data, w, h)
stream_segmentation_dataset.append(image)
batch_dataset[stream.name] = np.array(stream_rgb_dataset)
if stream_depth_dataset:
batch_dataset['%s_depth' % stream.name] = np.array(stream_depth_dataset)
if stream_normals_dataset:
batch_dataset['%s_normals' % stream.name] = np.array(stream_normals_dataset)
if stream_segmentation_dataset:
batch_dataset['%s_segmentation' % stream.name] = np.array(stream_segmentation_dataset)
for float_stream in response.auxiliary_float_streams:
_add_auxiliary_stream(batch_dataset, batch_size, float_stream, float)
for int_stream in response.auxiliary_int_streams:
_add_auxiliary_stream(batch_dataset, batch_size, int_stream, int)
for bool_stream in response.auxiliary_bool_streams:
_add_auxiliary_stream(batch_dataset, batch_size, bool_stream, bool)
return batch_dataset
def _build_update_request(renderer_config):
request = render_service_pb2.UpdateRequest()
for component in renderer_config.components:
component_copy = request.components.add()
component_copy.CopyFrom(component)
return request
class _WorkloadWithConfig:
def __init__(self, renderer_config_stamp, renderer_config, workload):
self.renderer_config_stamp = renderer_config_stamp
self.renderer_config = renderer_config
self.workload = workload
class _RemoteRendererWorker(QueueWorkerABC):
def __init__(self, input_queue, device, port, base_config):
super().__init__(input_queue)
self.device = device
self.port = port
self.base_config = base_config
self.server_process = None
self.client_stub = None
self.renderer_config_stamp = 0
def on_run(self):
if self.base_config.spawn_servers:
command, args, environment = self._server_commandline()
logging.info('Starting render server: %s args: %s env: %s.' % (
command, args, environment if environment else '-'))
environment_copy = os.environ.copy()
if environment:
environment_copy.update(environment)
if os.getenv('ORRB_DETACHED'):
print('\nManually start: %s' % ' '.join([command] + args))
input('\nPress enter when ready.')
else:
self.server_process = subprocess.Popen([command] + args,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=os.path.dirname(command),
env=environment_copy)
for i in range(10):
try:
self.client_stub = _create_render_service_stub(self.port)
return
except:
logging.warning(f'... cannot create client stub, retrying. If this persists '
f'check /tmp/StandaloneRenderer.{self.port}.log.')
time.sleep(1 + i)
raise RendererError('Could not create remote render server.')
def on_shutdown(self):
if self.server_process:
self.server_process.terminate()
self.server_process.kill()
self.server_process.wait()
def process(self, workload_with_config):
if workload_with_config.renderer_config_stamp != self.renderer_config_stamp:
self.renderer_config_stamp = workload_with_config.renderer_config_stamp
self.client_stub.Update(_build_update_request(workload_with_config.renderer_config))
actual_workload = workload_with_config.workload
request, batch_size = _build_render_batch_request(actual_workload, self.base_config)
response = self.client_stub.RenderBatch(request)
actual_workload.update(
_convert_render_batch_response(response, self.base_config, batch_size))
return actual_workload
def _server_commandline(self):
assert self.base_config.renderer_local_binary
assert self.base_config.model_xml_path
assert self.base_config.model_mapping_path
assert self.base_config.renderer_config_path
commandline_args = ['-logFile', '/tmp/StandaloneRenderer.%d.log' % self.port,
'--render_server.host=%s' % _get_server_bind_host(),
'--render_server.port=%d' % self.port,
'--render_server.workers_count=%d' % self.base_config.workers_count,
'--render_server.queues_count=%d' % self.base_config.queues_count,
'--render_server.streams_count=%d' % self.base_config.streams_count,
'--main.mode=Server',
'--main.model_xml_path=%s' % self.base_config.model_xml_path,
'--main.model_mapping_path=%s' % self.base_config.model_mapping_path,
'--main.renderer_config_path=%s' % (
self.base_config.renderer_config_path),
'--main.asset_basedir=%s' % self.base_config.asset_basedir,
'--main.parent_pid=%d' % os.getpid()]
display = os.getenv('ORRB_DISPLAY', '0')
environment = {'DISPLAY': ':%s.%d' % (display, self.device)}
return (self.base_config.renderer_local_binary, commandline_args, environment)
def _resolve_path(base_dir, path):
if os.path.isabs(path):
return path
else:
return os.path.normpath(os.path.join(os.path.abspath(base_dir), path))
def _load_renderer_config(base_dir, renderer_config_path):
resolved_renderer_config_path = _resolve_path(base_dir, renderer_config_path)
with open(resolved_renderer_config_path, 'r') as f:
config = f.read()
logging.info(f'Renderer config: {resolved_renderer_config_path}\n\n{config}')
return json_format.Parse(config, renderer_config_pb2.RendererConfig())
class RendererError(Exception):
pass
class RemoteRendererConfig:
def __init__(self):
self.camera_names = []
self.render_alpha = False
self.render_depth = False
self.render_normals = False
self.render_segmentation = False
self.image_width = 100
self.image_height = 100
self.spawn_servers = True
self.renderer_local_binary = None
self.renderer_version = None
self.renderer_config_path = None
self.model_xml_path = None
self.model_mapping_path = None
self.asset_basedir = "."
# Tune these params according to the request load placed upon each render server.
self.workers_count = 4
self.queues_count = 4
self.streams_count = 4
class RemoteRenderer(QueueExecutorABC):
def __init__(self, name, server_configs, base_config):
"""Main class for managing remote renderer workers and configs.
:param name: A name for this renderer in str.
:param server_configs: A list of (device, port), where device could be the GPU id.
:param base_config: A orrb.RemoteRendererConfig object.
"""
self.renderer_config_stamp = 0
self.renderer_config = _load_renderer_config(base_config.asset_basedir,
base_config.renderer_config_path)
self.local_config = deepcopy(base_config)
self.sync_queue = Queue()
if self.local_config.renderer_local_binary is None:
self.local_config.renderer_local_binary = get_renderer_executable(
self.local_config.renderer_version)
super().__init__(name, server_configs, self.local_config)
def mutable_renderer_config(self):
return self.renderer_config
def create_workers(self, input_queue, server_configs, base_config):
assert len(server_configs) > 0
if os.getenv('ORRB_MINIMAL'):
minimal_setup_size = int(os.getenv('ORRB_MINIMAL'))
server_configs = server_configs[:minimal_setup_size]
workers = []
for (device, port) in server_configs:
workers.append(_RemoteRendererWorker(input_queue, device, port, base_config))
return workers
def execute(self, workload, destination):
assert False, "Executor low level API is hidden by the renderer."
def render_batch_async(self, workload, destination):
workload_with_config = _WorkloadWithConfig(self.renderer_config_stamp,
self.renderer_config,
workload)
super().execute(workload_with_config, destination)
def render_batch(self, workload):
self.render_batch_async(workload, self.sync_queue)
result = self.sync_queue.get()
self.sync_queue.task_done()
return result
def update(self, renderer_config):
self.renderer_config_stamp += 1
self.renderer_config = deepcopy(renderer_config)
def get_renderer_executable(version):
version_override = os.getenv('ORRB_VERSION_OVERRIDE')
if version_override is not None:
logging.warning(f'Overriding ORRB version with: {version_override}')
version = version_override
if version is None:
raise ValueError('Cannot resolve renderer binary, version is None.')
orb_binaries_dir = os.getenv('ORRB_BINARIES_DIR')
if orb_binaries_dir is None:
raise ValueError('Point ORRB_BINARIES_DIR to the directory with built renderers.')
package = 'StandaloneRenderer-%s-%s-%s' % (platform.system(), platform.machine(), version)
package_executable = os.path.join(orb_binaries_dir, package, 'run.sh')
if not os.path.exists(package_executable):
raise ValueError('Missing run.sh in package: %s (%s).' % (package, orb_binaries_dir))
return package_executable