| ## @package dataio | |
| # Module caffe2.python.dataio | |
| """ | |
| Defines the base interface for reading and writing operations. | |
| Readers/Writers are objects that produce operations that read/write sequences | |
| of data. Each operation reads or writes a list of BlobReferences. | |
| Readers and Writers must be implemented such that read and write operations | |
| are atomic and thread safe. | |
| Examples of possible Readers and Writers: | |
| QueueReader, QueueWriter, | |
| DatasetReader, DatasetWriter, | |
| See `dataset.py` for an example of implementation. | |
| """ | |
| from __future__ import absolute_import | |
| from __future__ import division | |
| from __future__ import print_function | |
| from __future__ import unicode_literals | |
| from caffe2.python import core | |
| from caffe2.python.schema import Field, Struct, from_blob_list | |
| import numpy as np | |
| import time | |
| class Reader(object): | |
| """ | |
| Reader is an abstract class to be implemented in order to provide | |
| operations capable of iterating through a dataset or stream of data. | |
| A Reader must implement at least one operation, `read`, which | |
| adds operations to a net that read the next batch of data. Readers can | |
| optionally support the `reset` operation, which is useful when multiple | |
| passes over the data are required. | |
| """ | |
| def __init__(self, schema=None): | |
| if schema is not None: | |
| assert isinstance(schema, Field) | |
| self._schema = schema | |
| def schema(self): | |
| assert self._schema is not None, 'Schema not provided for this reader.' | |
| return self._schema | |
| def _set_schema(self, schema): | |
| self._schema = schema | |
| def setup_ex(self, init_net, finish_net): | |
| """Setup nets to run at task initialization and cleanup time. | |
| Args: | |
| global_init_net: A net invoked at task init time. | |
| global_finish_net: A net invoked at task cleanup time. | |
| """ | |
| pass | |
| def read_ex(self, local_init_net, local_finish_net): | |
| read_net = core.Net('reader_body') | |
| return ([read_net], ) + self.read(read_net) | |
| def read_record_ex(self, local_init_net, local_finish_net): | |
| nets, should_stop, fields = self.read_ex( | |
| local_init_net, local_finish_net) | |
| if self._schema: | |
| fields = from_blob_list(self._schema, fields) | |
| return nets, should_stop, fields | |
| def read(self, read_net): | |
| """Append operations to read_net that will read a batch from the | |
| underlying data soruce. | |
| Operations added to `read_net` must be thread safe and atomic, that is, | |
| it should be possible to clone `read_net` and run multiple instances of | |
| it in parallel. | |
| Args: | |
| read_net: the net that will be appended with read operations | |
| Returns: | |
| A tuple (should_stop, fields), with: | |
| should_stop: BlobReference pointing to a boolean scalar | |
| blob that indicates whether the read operation | |
| was succesfull or whether the end of data has | |
| been reached. | |
| fields: A tuple of BlobReference containing the latest batch | |
| of data that was read. | |
| """ | |
| raise NotImplementedError('Readers must implement `read`.') | |
| def reset(self, net): | |
| """Append operations to `net` that will reset the reader. | |
| This can be used to read the data multiple times. | |
| Not all readers support this operation. | |
| """ | |
| raise NotImplementedError('This reader cannot be resetted.') | |
| def read_record(self, read_net): | |
| should_stop, fields = self.read(read_net) | |
| if self._schema: | |
| fields = from_blob_list(self._schema, fields) | |
| return should_stop, fields | |
| def execution_step(self, reader_net_name=None, external_should_stop=None): | |
| """Create an execution step with a net containing read operators. | |
| The execution step will contain a `stop_blob` that knows how to stop | |
| the execution loop when end of data was reached. | |
| E.g.: | |
| read_step, fields = reader.execution_step() | |
| consume_net = core.Net('consume') | |
| consume_net.Print(fields[0], []) | |
| p = core.Plan('reader') | |
| p.AddStep(read_step.AddNet(consume_net)) | |
| core.RunPlan(p) | |
| Args: | |
| reader_net_name: (optional) the name of the reader_net to be | |
| created. The execution step will | |
| be named accordingly. | |
| Returns: | |
| A tuple (read_step, fields), with: | |
| read_step: A newly created execution step containing a net with | |
| read operations. The step will have `stop_blob` set, | |
| in order to stop the loop on end of data. | |
| fields: A tuple of BlobReference containing the latest batch | |
| of data that was read. | |
| """ | |
| reader_net = core.Net(reader_net_name or 'reader') | |
| should_stop, fields = self.read_record(reader_net) | |
| if external_should_stop is not None: | |
| should_stop = reader_net.Or([external_should_stop, should_stop]) | |
| read_step = core.execution_step( | |
| '{}_step'.format(reader_net_name), | |
| reader_net, | |
| should_stop_blob=should_stop) | |
| return (read_step, fields) | |
| class Writer(object): | |
| """ | |
| Writer is an abstract class to be implemented in order to provide | |
| operations capable of feeding a data stream or a dataset. | |
| A Writer must implement 2 operations: | |
| `write`, which adds operations to a net that write the write batch of | |
| data, and `commit`, which adds operations to a net in order to indicate | |
| that no more data will be written. | |
| """ | |
| _schema = None | |
| def schema(self): | |
| return self._schema | |
| def write(self, writer_net, fields): | |
| """Add operations to `writer_net` that write the next batch of data. | |
| Operations added to the net must be thread-safe and unique, that is: | |
| multiple writers must be able to write to the dataset in parallel. | |
| Args: | |
| fields: a tuple of BlobReference containing the batch of data to | |
| write. | |
| """ | |
| raise NotImplementedError('Writers must implement write.') | |
| def write_record(self, writer_net, fields): | |
| if isinstance(fields, Field): | |
| self._schema = fields | |
| fields = fields.field_blobs() | |
| self.write(writer_net, fields) | |
| def setup_ex(self, init_net, finish_net): | |
| """Experimental, don't use yet""" | |
| self.commit(finish_net) | |
| def write_ex(self, fields, local_init_net, local_finish_net, stop_blob): | |
| """Experimental extension to the interface. Don't use yet""" | |
| write_net = core.Net('write_net') | |
| self.write(write_net, fields) | |
| return [write_net] | |
| def write_record_ex( | |
| self, fields, local_init_net, local_finish_net, stop_blob=None): | |
| """Experimental extension to the interface. Don't use yet.""" | |
| if isinstance(fields, Field): | |
| self._schema = fields | |
| fields = fields.field_blobs() | |
| if stop_blob is None: | |
| stop_blob = local_init_net.NextName("dequeue_status") | |
| write_nets = self.write_ex( | |
| fields, local_init_net, local_finish_net, stop_blob) | |
| return (write_nets, stop_blob) | |
| def commit(self, finish_net): | |
| """Add operations to `finish_net` that signal end of data. | |
| This must be implemented by all Writers, but may be no-op for some | |
| of them. | |
| """ | |
| pass | |
| class ReaderBuilder(object): | |
| """ Allow usage of a reader in distributed fashion. """ | |
| def schema(self): | |
| raise NotImplementedError() | |
| def setup(self, **kwargs): | |
| """ | |
| Optionally, perform one-time setup before calling new_reader(). | |
| Subclass should make sure this function is only called once. | |
| """ | |
| raise NotImplementedError() | |
| def new_reader(self, **kwargs): | |
| raise NotImplementedError() | |
| class PipedReaderBuilder(ReaderBuilder): | |
| """ReaderBuilder that modifies underlying builder by calling `piper` | |
| function on each new reader produced, and return the result of | |
| the function. This way, it is possible to append data processing | |
| pipelines that will be replicated for each reader that gets created. | |
| E.g.: | |
| PipedReaderBuilder( | |
| ReaderBuilder(...), | |
| lambda reader: pipe(reader, processor=my_proc)) | |
| """ | |
| def __init__(self, builder, piper): | |
| self._builder = builder | |
| self._piper = piper | |
| def schema(self): | |
| return self._builder.schema() | |
| def setup(self, **kwargs): | |
| return self._builder.setup(**kwargs) | |
| def new_reader(self, **kwargs): | |
| # Passing everything down since you could wrap a PipedReaderBuilder in | |
| # another PipedReaderBuilder | |
| output = self._piper( | |
| reader=self._builder.new_reader(**kwargs), | |
| **kwargs | |
| ) | |
| return output if isinstance(output, Reader) else output.reader() | |
| class Pipe(object): | |
| def __init__(self, schema=None, obj_key=None): | |
| self._num_writers = 0 | |
| self._num_readers = 0 | |
| self._schema = schema | |
| self._obj_key = obj_key | |
| def schema(self): | |
| return self._schema | |
| def setup(self, global_init_net): | |
| pass | |
| def reader(self): | |
| raise NotImplementedError() | |
| def writer(self): | |
| raise NotImplementedError() | |
| def num_readers(self): | |
| return self._num_readers | |
| def num_writers(self): | |
| return self._num_writers | |
| def _new_writer(self, writer_schema, writer_init_net): | |
| if writer_schema is not None and self._schema is None: | |
| self._schema = writer_schema | |
| self._num_writers += 1 | |
| if self._obj_key is not None: | |
| writer_init_net.add_attribute(self._obj_key, self) | |
| def _new_reader(self, reader_init_net): | |
| self._num_readers += 1 | |
| if self._obj_key is not None: | |
| reader_init_net.add_attribute(self._obj_key, self) | |
| class CounterReader(Reader): | |
| """ Reader that produces increasing integers. """ | |
| def __init__(self): | |
| Reader.__init__(self, schema=Struct(('iter', np.int64))) | |
| self.counter = None | |
| self.should_stop = None | |
| def setup_ex(self, global_init_net, global_finish_net): | |
| if self.counter is None: | |
| self.counter = global_init_net.CreateCounter([], init_count=0) | |
| self.should_stop = global_init_net.ConstantFill( | |
| [], shape=[], dtype=core.DataType.BOOL, value=False) | |
| def read_ex(self, local_init_net, local_finish_net): | |
| count_net = core.Net('limited_reader_counter') | |
| value = count_net.CountUp([self.counter], 1) | |
| return [count_net], self.should_stop, [value] | |
| class ReaderWithLimitBase(Reader): | |
| """Abstract Reader constrained by certain conditions. | |
| Base class for Reader classes which check for certain conditions to stop | |
| further processing (e.g. max number of iterations or time limit). | |
| Also produces a boolean blob (data_finished) that can be used to see if | |
| the reader exausted all input data (true) or stopped for another reason | |
| (false). | |
| """ | |
| def __init__(self, reader): | |
| Reader.__init__(self, schema=reader._schema) | |
| self.reader = reader | |
| self.net = core.Net('reader_with_limit') | |
| self._data_finished = self.net.AddExternalInput( | |
| self.net.NextName('data_finished')) | |
| self.should_stop = None | |
| def setup_ex(self, global_init_net, global_finish_net): | |
| global_init_net.ConstantFill( | |
| [], [self._data_finished], | |
| shape=[], value=False, dtype=core.DataType.BOOL) | |
| self.reader.setup_ex(global_init_net, global_finish_net) | |
| self.setup_limiter(global_init_net, global_finish_net) | |
| def read_ex(self, local_init_net, local_finish_net): | |
| """Reads from an underlying Reader class, but may stop due to additional | |
| constraints. | |
| Build and return network(s) to read data from a Reader with | |
| additional constraints, depending on which derived class is used. | |
| Derived classes implement setup_limited and check_limiter_condition | |
| which determine the nature of the constraint imposed on the reader, | |
| e.g. iteration limits or time limit. | |
| Args: | |
| local_init_net: A net invoked at task instance init time (Once per | |
| parallel thread). | |
| local_finish_net: A net invoked at task instance cleanup time (Once | |
| per parallel thread). | |
| """ | |
| # Check if limiting constraint is met. | |
| stop_condition_net = core.Net('limited_reader_condition') | |
| should_stop = self.check_limiter_condition(stop_condition_net) | |
| # Call original reader. | |
| nets, local_data_finished, fields = self.reader.read_ex( | |
| local_init_net, local_finish_net) | |
| self._set_schema(self.reader._schema) | |
| # Check if original reader is done. | |
| check_done_net = core.Net('limited_reader_post') | |
| # Copy to the same blob as the counter output to trigger reader | |
| # stopping - this is ok because execution will check should_stop_blob | |
| # after every single operation, so it has already been checked on this | |
| # iteration by this point. | |
| check_done_net.Copy(local_data_finished, should_stop) | |
| # Update externally-accessible flag indicating if reader is done | |
| check_done_net.Or([self._data_finished, local_data_finished], | |
| [self._data_finished]) | |
| return [stop_condition_net] + nets + [check_done_net], should_stop, fields | |
| def setup_limiter(self, global_init_net, global_finish_net): | |
| """Configure task level init/cleanup nets required to implement limit | |
| condition. Must be implemented by subclass. | |
| Args: | |
| global_init_net: A net invoked at task init time. | |
| global_finish_net: A net invoked at task cleanup time. | |
| """ | |
| raise NotImplementedError("Subclass must implement `setup_limiter`") | |
| def check_limiter_condition(self, stop_condition_net): | |
| """Configure a net that is invoked between reading batches to see if | |
| limit condition is met. Must be implemented by subclass. | |
| Args: | |
| stop_condition_net: A net invoked to evaluate an early termination | |
| condition. | |
| """ | |
| raise NotImplementedError("Subclass must implement `check_limiter_condition") | |
| def data_finished(self): | |
| """ | |
| Return a blob that can be checked after the end of the reading task, | |
| which will contain a scalar float indicating whether the underlying | |
| reader has been exhausted (True) or whether we stopped because reached | |
| the limit of iterations (False). | |
| """ | |
| return self._data_finished | |
| class ReaderWithLimit(ReaderWithLimitBase): | |
| """Reader that stops after `num_iter` batches. | |
| If `num_iter` <= 0 or is None, reverts to an unconstrained reader that | |
| exports a boolean blob indicating that the reader has exhausted | |
| the data steam. | |
| """ | |
| def __init__(self, reader, num_iter=1): | |
| """Class initializer. | |
| Args: | |
| reader: The underlying reader object doing the actual read. | |
| num_iter: Number of batches to read. If `None`, | |
| the class reverts to a normal reader except that it also | |
| produces a data_finished blob as a side effect to indicate | |
| whether the input stream is exhausted. | |
| """ | |
| super(ReaderWithLimit, self).__init__(reader) | |
| self.counter = None | |
| self.num_iter = num_iter | |
| if self.num_iter is not None: | |
| self.counter = self.net.AddExternalInput( | |
| self.net.NextName('counter')) | |
| def setup_limiter(self, global_init_net, global_finish_net): | |
| if self.counter: | |
| global_init_net.CreateCounter( | |
| [], [self.counter], init_count=int(self.num_iter)) | |
| def check_limiter_condition(self, stop_condition_net): | |
| if self.counter: | |
| return stop_condition_net.CountDown([self.counter], 1) | |
| else: | |
| return stop_condition_net.ConstantFill( | |
| [], 1, | |
| shape=[], value=False, dtype=core.DataType.BOOL) | |
| def CountUntil(num_iter): | |
| return ReaderWithLimit(CounterReader(), num_iter) | |
| class ReaderWithTimeLimit(ReaderWithLimitBase): | |
| """Reader that stops after `duration` seconds. | |
| If `duration` <= 0 or is None, reverts to an unconstrained reader that | |
| exports a boolean blob indicating that the reader has exhausted | |
| the data steam. | |
| """ | |
| def __init__(self, reader, duration=0): | |
| """Class initializer. | |
| Args: | |
| reader: The underlying reader object doing the actual read. | |
| duration: Number of seconds to read. If un-specified, None, or <= 0, | |
| the class reverts to a normal reader except that it also | |
| produces a data_finished blob as a side effect to indicate | |
| whether the input stream is exhausted. | |
| """ | |
| super(ReaderWithTimeLimit, self).__init__(reader) | |
| self.timer = None | |
| self.duration = duration | |
| self.duration_ns_blob = None | |
| def setup_limiter(self, global_init_net, global_finish_net): | |
| if self.duration is not None and self.duration > 0: | |
| duration_ns = int(self.duration * (10**9)) | |
| self.timer = global_init_net.TimerBegin( | |
| [], counter_name='epoch_timer') | |
| start_time = global_init_net.TimerGet(self.timer) | |
| self.duration_ns_blob = global_init_net.ConstantFill( | |
| [start_time], value=duration_ns) | |
| global_finish_net.TimerEnd([self.timer], []) | |
| def check_limiter_condition(self, stop_condition_net): | |
| if self.duration: | |
| time_elapsed = stop_condition_net.TimerGet(self.timer) | |
| return stop_condition_net.GE( | |
| [time_elapsed, self.duration_ns_blob], str(self.should_stop)) | |
| else: | |
| return stop_condition_net.ConstantFill( | |
| [], 1, shape=[], value=False, dtype=core.DataType.BOOL | |
| ) | |
| class ReaderWithDelay(Reader): | |
| """Test reader class that inserts a delay between reading batches.""" | |
| def __init__(self, reader, delay): | |
| Reader.__init__(self, schema=reader._schema) | |
| self.reader = reader | |
| self.delay = delay | |
| def setup_ex(self, global_init_net, global_finish_net): | |
| self.reader.setup_ex(global_init_net, global_finish_net) | |
| def read_ex(self, local_init_net, local_finish_net): | |
| read_net = core.Net("reader_body") | |
| def sleep_op(*args, **argd): | |
| time.sleep(self.delay) | |
| read_net.Python(sleep_op)([], []) | |
| return ([read_net],) + self.reader.read(read_net) | |
| class CompositeReader(Reader): | |
| """ | |
| Base class for a reader that wrap multiple readers, e.g., reading from | |
| multiple sources simultaneously. | |
| """ | |
| def __init__(self, names, readers): | |
| """ | |
| Args: | |
| names: list[str] names of readers; used as schema keys | |
| readers: list[Reader] Reader instances, must have schema | |
| """ | |
| assert len(names) == len(readers) | |
| super(CompositeReader, self).__init__(schema=Struct(*[ | |
| (name, reader.schema()) for name, reader in zip(names, readers) | |
| ])) | |
| self._names = names | |
| self._readers = readers | |
| def setup_ex(self, init_net, finish_net): | |
| for reader in self._readers: | |
| reader.setup_ex(init_net, finish_net) | |
| def read_ex(self, local_init_net, local_finish_net): | |
| """ | |
| Stops when one of the reader finished | |
| """ | |
| local_should_stop = local_init_net.ConstantFill( | |
| [], shape=[], dtype=core.DataType.BOOL, value=False) | |
| read_nets = [] | |
| fields = [] | |
| for name, reader in zip(self._names, self._readers): | |
| sub_read_nets, should_stop, record = reader.read_record_ex( | |
| local_init_net, local_finish_net) | |
| stop_net = core.Net("{}_stop".format(name)) | |
| stop_net.Copy(should_stop, local_should_stop) | |
| sub_read_nets.append(stop_net) | |
| read_nets.extend(sub_read_nets) | |
| fields.extend(record.field_blobs()) | |
| return read_nets, local_should_stop, fields | |
| def reset(self, net): | |
| for reader in self._readers: | |
| reader.reset(net) | |
| class CompositeReaderBuilder(ReaderBuilder): | |
| """ | |
| A reader builder for CompositeReader | |
| """ | |
| def __init__(self, names, reader_builders): | |
| """ | |
| Args: | |
| names: list[str] names of readers; used as schema keys | |
| reader_builders: list[ReaderBuilder] ReaderBuilder instances; | |
| must have schema | |
| """ | |
| super(CompositeReaderBuilder, self).__init__() | |
| self._names = names | |
| self._reader_builders = reader_builders | |
| self._schema = Struct(*[ | |
| (name, reader_builder.schema()) | |
| for name, reader_builder in zip(names, reader_builders) | |
| ]) | |
| def schema(self): | |
| return self._schema | |
| def setup(self, **kwargs): | |
| for reader_builder in self._reader_builders: | |
| reader_builder.setup(**kwargs) | |
| # limiter is stateful; it can only be used once. Since | |
| # CompositeReader stops when one of the reader stops, | |
| # this is fine. | |
| if "limiter" in kwargs: | |
| kwargs.pop("limiter") | |
| def new_reader(self, **kwargs): | |
| readers = [] | |
| for reader_builder in self._reader_builders: | |
| reader = reader_builder.new_reader(**kwargs) | |
| if isinstance(reader, Reader): | |
| pass | |
| elif hasattr(reader, 'reader'): | |
| reader = reader.reader() | |
| else: | |
| raise ValueError('reader must be an instance of Reader or Pipe') | |
| readers.append(reader) | |
| multi_reader = CompositeReader(self._names, readers) | |
| assert multi_reader.schema() == self._schema | |
| return multi_reader |