support/retro_contest/docker.py (273 lines of code) (raw):
import argparse
import docker
import io
import os
import random
import requests.exceptions
import sys
import tempfile
import threading
try:
from retro import data_path
except ImportError:
def data_path():
raise RuntimeError('Could not find Gym Retro data directory')
class LogThread:
def __init__(self, container):
self._log = container.logs(stdout=True, stderr=True, stream=True)
self._thread = threading.Thread(target=self._run)
self._active = False
def start(self):
if self._active:
return
self._active = True
self._thread.start()
def exit(self):
self._active = False
def _run(self):
while self._active:
try:
print(next(self._log).decode('utf-8'), end='')
except StopIteration:
break
def convert_path(path):
if sys.platform.startswith('win') and path[1] == ':':
path = '/%s%s' % (path[0].lower(), path[2:].replace('\\', '/'))
return path
def run(game, state=None, entry=None, **kwargs):
client = docker.from_env()
remote_command = ['retro-contest-remote', 'run', game, *([state] if state else []), '-b', 'results/bk2', '-m', 'results']
remote_name = kwargs.get('remote_env', 'openai/retro-env')
agent_command = []
agent_name = kwargs.get('agent', 'agent')
datamount = {}
if kwargs.get('wallclock_limit') is not None:
remote_command.extend(['-W', str(kwargs['wallclock_limit'])])
if kwargs.get('timestep_limit') is not None:
remote_command.extend(['-T', str(kwargs['timestep_limit'])])
if kwargs.get('discrete_actions'):
remote_command.extend(['-D'])
if entry:
agent_command.append(entry)
if kwargs.get('entry_args'):
agent_command.extend(kwargs['entry_args'])
rand = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz0123456789', 8))
volname = 'retro-contest-tmp%s' % rand
datamount = {}
agentmount = {}
if kwargs.get('resultsdir'):
results = os.path.realpath(kwargs['resultsdir'])
datamount[convert_path(results)] = {'bind': '/root/compo/results'}
os.makedirs(results, exist_ok=True)
else:
results = None
if kwargs.get('agentdir'):
agentdir = os.path.realpath(kwargs['agentdir'])
agentmount[convert_path(agentdir)] = {'bind': '/root/compo/out'}
os.makedirs(agentdir, exist_ok=True)
container_kwargs = {'detach': True, 'network_disabled': True}
remote_kwargs = dict(container_kwargs)
agent_kwargs = dict(container_kwargs)
if kwargs.get('agent_shm'):
agent_kwargs['shm_size'] = kwargs['agent_shm']
bridge = client.volumes.create(volname, driver='local', driver_opts={'type': 'tmpfs', 'device': 'tmpfs'})
if kwargs.get('use_host_data'):
remote_command = [remote_command[0], '--data-dir', '/root/data', *remote_command[1:]]
datamount[convert_path(data_path())] = {'bind': '/root/data', 'mode': 'ro'}
try:
remote = client.containers.run(remote_name, remote_command,
volumes={volname: {'bind': '/root/compo/tmp'},
**datamount},
**remote_kwargs)
except:
bridge.remove()
raise
try:
agent = client.containers.run(agent_name, agent_command,
volumes={volname: {'bind': '/root/compo/tmp'},
**agentmount},
runtime=kwargs.get('runtime', 'nvidia'),
**agent_kwargs)
except:
remote.kill()
remote.remove()
bridge.remove()
raise
a_exit = None
r_exit = None
if not kwargs.get('quiet'):
log_thread = LogThread(agent)
log_thread.start()
try:
while True:
try:
a_exit = agent.wait(timeout=5)
break
except requests.exceptions.RequestException:
pass
try:
r_exit = remote.wait(timeout=5)
break
except requests.exceptions.RequestException:
pass
if a_exit is None:
try:
a_exit = agent.wait(timeout=10)
except requests.exceptions.RequestException:
agent.kill()
if r_exit is None:
try:
r_exit = remote.wait(timeout=10)
except requests.exceptions.RequestException:
remote.kill()
except:
if a_exit is None:
try:
a_exit = agent.wait(timeout=1)
except:
try:
agent.kill()
except docker.errors.APIError:
pass
if r_exit is None:
try:
r_exit = remote.wait(timeout=1)
except:
try:
remote.kill()
except docker.errors.APIError:
pass
raise
finally:
if isinstance(a_exit, dict):
a_exit = a_exit.get('StatusCode')
if isinstance(r_exit, dict):
r_exit = r_exit.get('StatusCode')
if not kwargs.get('quiet'):
log_thread.exit()
logs = {
'remote': (r_exit, remote.logs(stdout=True, stderr=False), remote.logs(stdout=False, stderr=True)),
'agent': (a_exit, agent.logs(stdout=True, stderr=False), agent.logs(stdout=False, stderr=True))
}
if results:
with open(os.path.join(results, 'remote-stdout.txt'), 'w') as f:
f.write(logs['remote'][1].decode('utf-8'))
with open(os.path.join(results, 'remote-stderr.txt'), 'w') as f:
f.write(logs['remote'][2].decode('utf-8'))
with open(os.path.join(results, 'agent-stdout.txt'), 'w') as f:
f.write(logs['agent'][1].decode('utf-8'))
with open(os.path.join(results, 'agent-stderr.txt'), 'w') as f:
f.write(logs['agent'][2].decode('utf-8'))
remote.remove()
agent.remove()
bridge.remove()
return logs
def run_args(args):
kwargs = {
'entry_args': args.args,
'wallclock_limit': args.wallclock_limit,
'timestep_limit': args.timestep_limit,
'discrete_actions': args.discrete_actions,
'resultsdir': args.results_dir,
'agentdir': args.agent_dir,
'quiet': args.quiet,
'use_host_data': args.use_host_data,
'agent_shm': args.agent_shm,
}
if args.no_nv:
kwargs['runtime'] = None
if args.agent:
kwargs['agent'] = args.agent
if args.remote_env:
kwargs['remote_env'] = args.remote_env
results = run(args.game, args.state, args.entry, **kwargs)
if results['remote'][0] or results['agent'][0]:
if results['remote'][0]:
print('Remote exited uncleanly:', results['remote'][0])
if results['agent'][0]:
print('Agent exited uncleanly', results['agent'][0])
return False
return True
def build(path, tag, install=None, pass_env=False):
from pkg_resources import EntryPoint
import tarfile
if install:
destination = 'module'
else:
destination = 'agent.py'
docker_file = ['FROM openai/retro-agent',
'COPY context %s' % destination]
if not install:
docker_file.append('CMD ["python", "-u", "/root/compo/agent.py"]')
else:
docker_file.append('RUN . ~/venv/bin/activate && pip install -e module')
valid = not any(c in install for c in ' "\\')
if pass_env:
try:
EntryPoint.parse('entry=' + install)
except ValueError:
valid = False
if not valid:
raise ValueError('Invalid entry point')
docker_file.append('CMD ["retro-contest-agent", "%s"]' % install)
else:
if not valid:
raise ValueError('Invalid module name')
docker_file.append('CMD ["python", "-u", "-m", "%s"]' % install)
print('Creating Docker image...')
docker_file_full = io.BytesIO('\n'.join(docker_file).encode('utf-8'))
client = docker.from_env()
with tempfile.NamedTemporaryFile() as f:
tf = tarfile.open(mode='w:gz', fileobj=f)
docker_file_info = tarfile.TarInfo('Dockerfile')
docker_file_info.size = len(docker_file_full.getvalue())
tf.addfile(docker_file_info, docker_file_full)
tf.add(path, arcname='context', exclude=lambda fname: fname.endswith('/.git'))
tf.close()
f.seek(0)
client.images.build(fileobj=f, custom_context=True, tag=tag, gzip=True)
print('Done!')
def build_args(args):
kwargs = {
'install': args.install,
'pass_env': args.pass_env,
}
try:
build(args.path, args.tag, **kwargs)
except docker.errors.BuildError as be:
print(*[log['stream'] for log in be.build_log if 'stream' in log])
raise
return True
def init_parser(subparsers):
parser_run = subparsers.add_parser('run', description='Run Docker containers locally')
parser_run.set_defaults(func=run_args)
parser_run.add_argument('game', type=str, help='Name of the game to run')
parser_run.add_argument('state', type=str, default=None, nargs='?', help='Name of initial state')
parser_run.add_argument('--entry', '-e', type=str, help='Name of agent entry point')
parser_run.add_argument('--args', '-A', type=str, nargs='+', help='Extra agent entry arguments')
parser_run.add_argument('--agent', '-a', type=str, help='Extra agent Docker image')
parser_run.add_argument('--wallclock-limit', '-W', type=float, default=None, help='Maximum time to run in seconds')
parser_run.add_argument('--timestep-limit', '-T', type=int, default=None, help='Maximum time to run in timesteps')
parser_run.add_argument('--no-nv', '-N', action='store_true', help='Disable Nvidia runtime')
parser_run.add_argument('--remote-env', '-R', type=str, help='Remote Docker image')
parser_run.add_argument('--results-dir', '-r', type=str, help='Path to output results')
parser_run.add_argument('--agent-dir', '-o', type=str, help='Path to mount into agent (mounted at /root/compo/out)')
parser_run.add_argument('--discrete-actions', '-D', action='store_true', help='Use a discrete action space')
parser_run.add_argument('--use-host-data', '-d', action='store_true', help='Use the host Gym Retro data directory')
parser_run.add_argument('--quiet', '-q', action='store_true', help='Disable printing agent logs')
parser_run.add_argument('--agent-shm', type=str, help='Agent /dev/shm size')
parser_build = subparsers.add_parser('build', description='Build agent Docker containers')
parser_build.set_defaults(func=build_args)
parser_build.add_argument('path', type=str, help='Path to a file or package')
parser_build.add_argument('--tag', '-t', required=True, type=str, help='Tag name for the built image')
parser_build.add_argument('--install', '-i', type=str, help='Install as a package and run specified module or entry point (if -e is specified)')
parser_build.add_argument('--pass-env', '-e', action='store_true', help='Pass preconfigured environment to entry point specified by -i')
def main(argv=sys.argv[1:]):
parser = argparse.ArgumentParser(description='Run OpenAI Retro Contest support code')
parser.set_defaults(func=lambda args: parser.print_help())
init_parser(parser.add_subparsers())
args = parser.parse_args(argv)
if not args.func(args):
sys.exit(1)
if __name__ == '__main__':
main()