Example #1
0
    def test_gpot_ports(self):
        i = Interface('/foo[0:6]')
        i['/foo[0]'] = [0, 'in', 'spike']
        i['/foo[1:3]'] = [0, 'out', 'spike']
        i['/foo[3]'] = [0, 'in', 'gpot']
        i['/foo[4:6]'] = [0, 'out', 'gpot']
        j = Interface('/foo[3:6]')
        j['/foo[3]'] = [0, 'in', 'gpot']
        j['/foo[4:6]'] = [0, 'out', 'gpot']

        # Test returning result as Interface:
        assert_frame_equal(i.gpot_ports(0).data, j.data)

        # Test returning result as list of tuples:
        self.assertItemsEqual(i.gpot_ports(0, True), j.data.index.tolist())
Example #2
0
    def test_gpot_ports(self):
        i = Interface('/foo[0:6]')
        i['/foo[0]'] = [0, 'in', 'spike']
        i['/foo[1:3]'] = [0, 'out', 'spike']
        i['/foo[3]'] = [0, 'in', 'gpot']
        i['/foo[4:6]'] = [0, 'out', 'gpot']
        j = Interface('/foo[3:6]')
        j['/foo[3]'] = [0, 'in', 'gpot']
        j['/foo[4:6]'] = [0, 'out', 'gpot']

        # Test returning result as Interface:
        assert_frame_equal(i.gpot_ports(0).data, j.data)

        # Test returning result as list of tuples:
        self.assertItemsEqual(i.gpot_ports(0, True),
                              j.data.index.tolist())
Example #3
0
 def test_gpot_ports(self):
     i = Interface('/foo[0:6]')
     i['/foo[0]'] = [0, 'in', 'spike']
     i['/foo[1:3]'] = [0, 'out', 'spike']
     i['/foo[3]'] = [0, 'in', 'gpot']
     i['/foo[4:6]'] = [0, 'out', 'gpot']
     j = Interface('/foo[3:6]')
     j['/foo[3]'] = [0, 'in', 'gpot']
     j['/foo[4:6]'] = [0, 'out', 'gpot']
     assert_frame_equal(i.gpot_ports(0).data, j.data)
     assert_index_equal(i.gpot_ports(0).index, j.index)
Example #4
0
class Module(mpi.Worker):
    """
    Processing module.

    This class repeatedly executes a work method until it receives a
    quit message via its control network port.

    Parameters
    ----------
    sel : str, unicode, or sequence
        Path-like selector describing the module's interface of
        exposed ports.
    sel_in, sel_out, sel_gpot, sel_spike : str, unicode, or sequence
        Selectors respectively describing all input, output, graded potential,
        and spiking ports in the module's interface.
    data_gpot, data_spike : numpy.ndarray
        Data arrays associated with the graded potential and spiking ports in
        the . Array length must equal the number
        of ports in a module's interface.
    columns : list of str
        Interface port attributes.
        Network port for controlling the module instance.
    ctrl_tag, gpot_tag, spike_tag : int
        MPI tags that respectively identify messages containing control data,
        graded potential port values, and spiking port values transmitted to
        worker nodes.
    id : str
        Module identifier. If no identifier is specified, a unique
        identifier is automatically generated.
    device : int
        GPU device to use. May be set to None if the module does not perform
        GPU processing.
    routing_table : neurokernel.routing_table.RoutingTable
        Routing table describing data connections between modules. If no routing
        table is specified, the module will be executed in isolation.
    rank_to_id : bidict.bidict
        Mapping between MPI ranks and module object IDs.
    debug : bool
        Debug flag. When True, exceptions raised during the work method
        are not be suppressed.
    time_sync : bool
        Time synchronization flag. When True, debug messages are not emitted
        during module synchronization and the time taken to receive all incoming
        data is computed.

    Attributes
    ----------
    interface : Interface
        Object containing information about a module's ports.
    pm : dict
        `pm['gpot']` and `pm['spike']` are instances of neurokernel.pm_gpu.PortMapper that
        map a module's ports to the contents of the values in `data`.
    data : dict
        `data['gpot']` and `data['spike']` are arrays of data associated with
        a module's graded potential and spiking ports.
    """

    def __init__(self, sel, sel_in, sel_out,
                 sel_gpot, sel_spike, data_gpot, data_spike,
                 columns=['interface', 'io', 'type'],
                 ctrl_tag=CTRL_TAG, gpot_tag=GPOT_TAG, spike_tag=SPIKE_TAG,
                 id=None, device=None,
                 routing_table=None, rank_to_id=None,
                 debug=False, time_sync=False, print_timing=False):

        super(Module, self).__init__(ctrl_tag)
        self.debug = debug
        self.time_sync = time_sync
        self.device = device
        self.print_timing = print_timing

        self._gpot_tag = gpot_tag
        self._spike_tag = spike_tag

        # Require several necessary attribute columns:
        if 'interface' not in columns:
            raise ValueError('interface column required')
        if 'io' not in columns:
            raise ValueError('io column required')
        if 'type' not in columns:
            raise ValueError('type column required')

        # Initialize GPU here so as to be able to initialize a port mapper
        # containing GPU memory:
        self._init_gpu()

        # This is needed to ensure that MPI_Finalize is called before PyCUDA
        # attempts to clean up; see
        # https://groups.google.com/forum/#!topic/mpi4py/by0Rd5q0Ayw
        atexit.register(MPI.Finalize)

        # Manually register the file close method associated with MPIOutput
        # so that it is called by atexit before MPI.Finalize() (if the file is
        # closed after MPI.Finalize() is called, an error will occur):
        for k, v in iteritems(twiggy.emitters):
             if isinstance(v._output, MPIOutput):
                 atexit.register(v._output.close)

        # Ensure that the input and output port selectors respectively
        # select mutually exclusive subsets of the set of all ports exposed by
        # the module:
        if not SelectorMethods.is_in(sel_in, sel):
            raise ValueError('input port selector not in selector of all ports')
        if not SelectorMethods.is_in(sel_out, sel):
            raise ValueError('output port selector not in selector of all ports')
        if not SelectorMethods.are_disjoint(sel_in, sel_out):
            raise ValueError('input and output port selectors not disjoint')

        #assert(SelectorMethods.is_in(sel_in, sel))
        #assert(SelectorMethods.is_in(sel_out, sel))
        #assert(SelectorMethods.are_disjoint(sel_in, sel_out))

        # Ensure that the graded potential and spiking port selectors
        # respectively select mutually exclusive subsets of the set of all ports
        # exposed by the module:
        #assert(SelectorMethods.is_in(sel_gpot, sel))
        #assert(SelectorMethods.is_in(sel_spike, sel))
        #assert(SelectorMethods.are_disjoint(sel_gpot, sel_spike))

        # Save routing table and mapping between MPI ranks and module IDs:
        self.routing_table = routing_table
        self.rank_to_id = rank_to_id

        # Generate a unique ID if none is specified:
        if id is None:
            self.id = uid()
        else:

            # If a unique ID was specified and the routing table is not empty
            # (i.e., there are connections between multiple modules), the id
            # must be a node in the routing table:
            if routing_table is not None and len(routing_table.ids) and \
                    not routing_table.has_node(id):
                raise ValueError('routing table must contain specified '
                                 'module ID: {}'.format(id))
            self.id = id

        # Reformat logger name:
        LoggerMixin.__init__(self, 'mod %s' % self.id)

        if self.print_timing:
            start = time.time()

        # Create module interface given the specified ports:
        self.interface = Interface(sel, columns)
        # Set the interface ID to 0; we assume that a module only has one interface:
        self.interface[sel, 'interface'] = 0

        # Set the port attributes:
        if len(sel_in):
            self.interface[sel_in, 'io'] = 'in'
        if len(sel_out):
            self.interface[sel_out, 'io'] = 'out'
        if len(sel_gpot):
            self.interface[sel_gpot, 'type'] = 'gpot'
        if len(sel_spike):
            self.interface[sel_spike, 'type'] = 'spike'

        if self.print_timing:
            self.log_info('Elapsed time for setting up interface: {:.3f} seconds'.format(time.time()-start))
            start = time.time()

        # Find the graded potential and spiking ports:
        self.gpot_ports = self.interface.gpot_ports()
        self.spike_ports = self.interface.spike_ports()

        if len(self.gpot_ports):
            self.in_gpot_ports = self.gpot_ports.in_ports(tuples=True)
            self.out_gpot_ports = self.gpot_ports.out_ports(tuples=True)
        else:
            self.in_gpot_ports = []
            self.out_gpot_ports = []
        if len(self.spike_ports):
            self.in_spike_ports = self.spike_ports.in_ports(tuples=True)
            self.out_spike_ports = self.spike_ports.out_ports(tuples=True)
        else:
            self.in_spike_ports = []
            self.out_spike_ports = []

        if self.print_timing:
            self.log_info('Elapsed time for extracting ports: {:.3f} seconds'.format(time.time()-start))
            start = time.time()

        # Set up mapper between port identifiers and their associated data:
        if len(data_gpot) != len(self.gpot_ports):
            raise ValueError('incompatible gpot port data array length')
        if len(data_spike) != len(self.spike_ports):
            raise ValueError('incompatible spike port data array length')
        self.data = {}
        self.data['gpot'] = gpuarray.to_gpu(data_gpot)
        self.data['spike'] = gpuarray.to_gpu(data_spike)

        self.pm = {}
        self.pm['gpot'] = GPUPortMapper(sel_gpot, self.data['gpot'], make_copy=False)
        self.pm['spike'] = GPUPortMapper(sel_spike, self.data['spike'], make_copy=False)
        if self.print_timing:
            cuda.Context.synchronize()
            self.log_info('Elapsed time for creating array and PortMapper {} seconds'.format(time.time()-start))


    def _init_gpu(self):
        """
        Initialize GPU device.

        Notes
        -----
        Must be called from within the `run()` method, not from within
        `__init__()`.
        """

        if self.device == None:
            self.log_info('no GPU specified - not initializing ')
        else:

            # Import pycuda.driver here so as to facilitate the
            # subclassing of Module to create pure Python LPUs that don't use GPUs:
            import pycuda.driver as drv
            drv.init()

            N_gpu = drv.Device.count()
            if not self.device < N_gpu:
                new_device = randint(0,N_gpu - 1)
                self.log_warning("GPU device device %d not in GPU devices %s" % (self.device, str(range(0,N_gpu))))
                self.log_warning("Setting device = %d" % new_device)
                self.device = new_device

            try:
                self.gpu_ctx = drv.Device(self.device).make_context()
            except Exception as e:
                self.log_info('_init_gpu exception: ' + e.message)
            else:
                atexit.register(self.gpu_ctx.pop)
                self.log_info('GPU %s initialized' % self.device)

    def _init_port_dicts(self):
        """
        Initial dictionaries of source/destination ports in current module.
        """

        if self.print_timing:
            start = time.time()
        # Extract identifiers of source ports in the current module's interface
        # for all modules receiving output from the current module:
        self._out_port_dict_ids = {}
        self._out_port_dict_ids['gpot'] = {}
        self._out_port_dict_ids['spike'] = {}

        self._out_ids = self.routing_table.dest_ids(self.id)
        self._out_ranks = [self.rank_to_id.inv[i] for i in self._out_ids]
        for out_id in self._out_ids:
            self.log_info('extracting output ports for %s' % out_id)

            # Get interfaces of pattern connecting the current module to
            # destination module `out_id`; `int_0` is connected to the
            # current module, `int_1` is connected to the other module:
            pat = self.routing_table[self.id, out_id]['pattern']
            int_0 = self.routing_table[self.id, out_id]['int_0']
            int_1 = self.routing_table[self.id, out_id]['int_1']

            # Get ports in interface (`int_0`) connected to the current
            # module that are connected to the other module via the pattern:
            self._out_port_dict_ids['gpot'][out_id] = \
                gpuarray.to_gpu(self.pm['gpot'].ports_to_inds(pat.src_idx(int_0, int_1, 'gpot', 'gpot')))
            self._out_port_dict_ids['spike'][out_id] = \
                gpuarray.to_gpu(self.pm['spike'].ports_to_inds(pat.src_idx(int_0, int_1, 'spike', 'spike')))

        if self.print_timing:
            cuda.Context.synchronize()
            self.log_info('Elapsed time for extracting output ports: {:.3f} seconds'.format(time.time()-start))
            start = time.time()
        # Extract identifiers of destination ports in the current module's
        # interface for all modules sending input to the current module:
        self._in_port_dict_ids = {}
        self._in_port_dict_ids['gpot'] = {}
        self._in_port_dict_ids['spike'] = {}

        # Extract indices corresponding to the entries in the transmitted
        # buffers that must be copied into the input port map data arrays; these
        # are needed to support fan-out:
        self._in_port_dict_buf_ids = {}
        self._in_port_dict_buf_ids['gpot'] = {}
        self._in_port_dict_buf_ids['spike'] = {}

        # Lengths of input buffers:
        self._in_buf_len = {}
        self._in_buf_len['gpot'] = {}
        self._in_buf_len['spike'] = {}

        self._in_ids = self.routing_table.src_ids(self.id)
        self._in_ranks = [self.rank_to_id.inv[i] for i in self._in_ids]
        for in_id in self._in_ids:
            self.log_info('extracting input ports for %s' % in_id)

            # Get interfaces of pattern connecting the current module to
            # source module `in_id`; `int_1` is connected to the current
            # module, `int_0` is connected to the other module:
            pat = self.routing_table[in_id, self.id]['pattern']
            int_0 = self.routing_table[in_id, self.id]['int_0']
            int_1 = self.routing_table[in_id, self.id]['int_1']

            # Get ports in interface (`int_1`) connected to the current
            # module that are connected to the other module via the pattern:
            self._in_port_dict_ids['gpot'][in_id] = \
                gpuarray.to_gpu(self.pm['gpot'].ports_to_inds(pat.dest_idx(int_0, int_1, 'gpot', 'gpot')))
            self._in_port_dict_ids['spike'][in_id] = \
                gpuarray.to_gpu(self.pm['spike'].ports_to_inds(pat.dest_idx(int_0, int_1, 'spike', 'spike')))

			# Get the integer indices associated with the connected source ports
            # in the pattern interface connected to the source module `in_d`;
            # these are needed to copy received buffer contents into the current
            # module's port map data array:
            src_idx_dup = pat.src_idx(int_0, int_1, 'gpot', 'gpot', duplicates=True)
            src_idx = OrderedDict.fromkeys(src_idx_dup).keys()
            self._in_port_dict_buf_ids['gpot'][in_id] = \
                np.array(renumber_in_order(BasePortMapper(pat.gpot_ports(int_0).to_tuples()).
                        ports_to_inds(src_idx_dup)))
            src_idx_dup_s = pat.src_idx(int_0, int_1, 'spike', 'spike', duplicates=True)
            src_idx_s = OrderedDict.fromkeys(src_idx_dup_s).keys()
            self._in_port_dict_buf_ids['spike'][in_id] = \
                np.array(renumber_in_order(BasePortMapper(pat.spike_ports(int_0).to_tuples()).
                        ports_to_inds(src_idx_dup_s)))

            # The size of the input buffer to the current module must be the
            # same length as the output buffer of module `in_id`:
            self._in_buf_len['gpot'][in_id] = len(src_idx)
            self._in_buf_len['spike'][in_id] = len(src_idx_s)
            if self.print_timing:
                self.log_info('Elapsed time for extracting input ports: {:.3f} seconds'.format(time.time()-start))

    def _init_comm_bufs(self):
        """
        Buffers for sending/receiving data from other modules.

        Notes
        -----
        Must be executed after `_init_port_dicts()`.
        """

        # Buffers (and their interfaces and MPI types) for receiving data
        # transmitted from source modules:
        self._in_buf = {}
        self._in_buf['gpot'] = {}
        self._in_buf['spike'] = {}
        self._in_buf_int = {}
        self._in_buf_int['gpot'] = {}
        self._in_buf_int['spike'] = {}
        self._in_buf_mtype = {}
        self._in_buf_mtype['gpot'] = {}
        self._in_buf_mtype['spike'] = {}
        for in_id in self._in_ids:
            n_gpot = self._in_buf_len['gpot'][in_id]
            if n_gpot:
                self._in_buf['gpot'][in_id] = \
                    gpuarray.empty(n_gpot, self.pm['gpot'].dtype)
                self._in_buf_int['gpot'][in_id] = \
                    bufint(self._in_buf['gpot'][in_id])
                self._in_buf_mtype['gpot'][in_id] = \
                    dtype_to_mpi(self._in_buf['gpot'][in_id].dtype)
            else:
                self._in_buf['gpot'][in_id] = None

            n_spike = self._in_buf_len['spike'][in_id]
            if n_spike:
                self._in_buf['spike'][in_id] = \
                    gpuarray.empty(n_spike, self.pm['spike'].dtype)
                self._in_buf_int['spike'][in_id] = \
                    bufint(self._in_buf['spike'][in_id])
                self._in_buf_mtype['spike'][in_id] = \
                    dtype_to_mpi(self._in_buf['spike'][in_id].dtype)
            else:
                self._in_buf['spike'][in_id] = None

        # Buffers (and their interfaces and MPI types) for transmitting data to
        # destination modules:
        self._out_buf = {}
        self._out_buf['gpot'] = {}
        self._out_buf['spike'] = {}
        self._out_buf_int = {}
        self._out_buf_int['gpot'] = {}
        self._out_buf_int['spike'] = {}
        self._out_buf_mtype = {}
        self._out_buf_mtype['gpot'] = {}
        self._out_buf_mtype['spike'] = {}
        for out_id in self._out_ids:
            n_gpot = len(self._out_port_dict_ids['gpot'][out_id])
            if n_gpot:
                self._out_buf['gpot'][out_id] = \
                    gpuarray.empty(n_gpot, self.pm['gpot'].dtype)
                self._out_buf_int['gpot'][out_id] = \
                    bufint(self._out_buf['gpot'][out_id])
                self._out_buf_mtype['gpot'][out_id] = \
                    dtype_to_mpi(self._out_buf['gpot'][out_id].dtype)
            else:
                self._out_buf['gpot'][out_id] = None

            n_spike = len(self._out_port_dict_ids['spike'][out_id])
            if n_spike:
                self._out_buf['spike'][out_id] = \
                    gpuarray.empty(n_spike, self.pm['spike'].dtype)
                self._out_buf_int['spike'][out_id] = \
                    bufint(self._out_buf['spike'][out_id])
                self._out_buf_mtype['spike'][out_id] = \
                    dtype_to_mpi(self._out_buf['spike'][out_id].dtype)
            else:
                self._out_buf['spike'][out_id] = None

    def _sync(self):
        """
        Send output data and receive input data.
        """

        if self.time_sync:
            start = time.time()
        requests = []

        # For each destination module, extract elements from the current
        # module's port data array, copy them to a contiguous array, and
        # transmit the latter:
        for dest_id, dest_rank in zip(self._out_ids, self._out_ranks):

            # Copy data into destination buffer:
            if self._out_buf['gpot'][dest_id] is not None:
                set_by_inds(self._out_buf['gpot'][dest_id],
                            self._out_port_dict_ids['gpot'][dest_id],
                            self.data['gpot'], 'src')
                if not self.time_sync:
                    self.log_info('gpot data sent to %s: %s' % \
                                  (dest_id, str(self._out_buf['gpot'][dest_id])))
                r = MPI.COMM_WORLD.Isend([self._out_buf_int['gpot'][dest_id],
                                          self._out_buf_mtype['gpot'][dest_id]],
                                         dest_rank, GPOT_TAG)
                requests.append(r)
            if self._out_buf['spike'][dest_id] is not None:
                set_by_inds(self._out_buf['spike'][dest_id],
                            self._out_port_dict_ids['spike'][dest_id],
                            self.data['spike'], 'src')
                if not self.time_sync:
                    self.log_info('spike data sent to %s: %s' % \
                                  (dest_id, str(self._out_buf['spike'][dest_id])))
                r = MPI.COMM_WORLD.Isend([self._out_buf_int['spike'][dest_id],
                                          self._out_buf_mtype['spike'][dest_id]],
                                         dest_rank, SPIKE_TAG)
                requests.append(r)
            if not self.time_sync:
                self.log_info('sending to %s' % dest_id)
        if not self.time_sync:
            self.log_info('sent all data from %s' % self.id)

        # For each source module, receive elements and copy them into the
        # current module's port data array:
        for src_id, src_rank in zip(self._in_ids, self._in_ranks):
            if self._in_buf['gpot'][src_id] is not None:
                r = MPI.COMM_WORLD.Irecv([self._in_buf_int['gpot'][src_id],
                                          self._in_buf_mtype['gpot'][src_id]],
                                         source=src_rank, tag=GPOT_TAG)
                requests.append(r)
            if self._in_buf['spike'][src_id] is not None:
                r = MPI.COMM_WORLD.Irecv([self._in_buf_int['spike'][src_id],
                                          self._in_buf_mtype['spike'][src_id]],
                                         source=src_rank, tag=SPIKE_TAG)
                requests.append(r)
            if not self.time_sync:
                self.log_info('receiving from %s' % src_id)
        if requests:
            req = MPI.Request()
            req.Waitall(requests)
        if not self.time_sync:
            self.log_info('all data were received by %s' % self.id)

        # Copy received elements into the current module's data array:
        for src_id in self._in_ids:
            if self._in_buf['gpot'][src_id] is not None:
                if not self.time_sync:
                    self.log_info('gpot data received from %s: %s' % \
                                  (src_id, str(self._in_buf['gpot'][src_id])))
                set_by_inds_from_inds(self.data['gpot'],
                                      self._in_port_dict_ids['gpot'][src_id],
                                      self._in_buf['gpot'][src_id],
                                      self._in_port_dict_buf_ids['gpot'][src_id])
            if self._in_buf['spike'][src_id] is not None:
                if not self.time_sync:
                    self.log_info('spike data received from %s: %s' % \
                                  (src_id, str(self._in_buf['spike'][src_id])))
                set_by_inds_from_inds(self.data['spike'],
                                      self._in_port_dict_ids['spike'][src_id],
                                      self._in_buf['spike'][src_id],
                                      self._in_port_dict_buf_ids['spike'][src_id])

        # Save timing data:
        if self.time_sync:
            stop = time.time()
            n_gpot = 0
            n_spike = 0
            for src_id in self._in_ids:
                n_gpot += len(self._in_buf['gpot'][src_id])
                n_spike += len(self._in_buf['spike'][src_id])
            self.log_info('sent timing data to master')
            self.intercomm.isend(['sync_time',
                                  (self.rank, self.steps, start, stop,
                                   n_gpot*self.pm['gpot'].dtype.itemsize+\
                                   n_spike*self.pm['spike'].dtype.itemsize)],
                                 dest=0, tag=self._ctrl_tag)
        else:
            self.log_info('saved all data received by %s' % self.id)

    def pre_run(self):
        """
        Code to run before main loop.

        This method is invoked by the `run()` method before the main loop is
        started.
        """

        # MPI Request object for resolving asynchronous transfers:
        #self.req = MPI.Request()

        self.log_info('running code before body of worker %s' % self.rank)

        # Initialize _out_port_dict and _in_port_dict attributes:
        self._init_port_dicts()

        # Initialize transmission buffers:
        self._init_comm_bufs()

        # Start timing the main loop:
        if self.time_sync:
            self.intercomm.isend(['start_time', (self.rank, time.time())],
                                 dest=0, tag=self._ctrl_tag)
            self.log_info('sent start time to manager')

    def post_run(self):
        """
        Code to run after main loop.

        This method is invoked by the `run()` method after the main loop is
        started.
        """

        self.log_info('running code after body of worker %s' % self.rank)

        # Stop timing the main loop before shutting down the emulation:
        if self.time_sync:
            self.intercomm.isend(['stop_time', (self.rank, time.time())],
                                 dest=0, tag=self._ctrl_tag)

            self.log_info('sent stop time to manager')

        # Send acknowledgment message:
        self.intercomm.isend(['done', self.rank], 0, self._ctrl_tag)
        self.log_info('done message sent to manager')

    def run_step(self):
        """
        Module work method.

        This method should be implemented to do something interesting with new
        input port data in the module's `pm` attribute and update the attribute's
        output port data if necessary. It should not interact with any other
        class attributes.
        """

        self.log_info('running execution step')

    def run(self):
        """
        Body of process.
        """

        # Don't allow keyboard interruption of process:
        with IgnoreKeyboardInterrupt():

            # Activate execution loop:
            super(Module, self).run()

    def do_work(self):
        """
        Work method.

        This method is repeatedly executed by the Worker instance after the
        instance receives a 'start' control message and until it receives a 'stop'
        control message.
        """

        # If the debug flag is set, don't catch exceptions so that
        # errors will lead to visible failures:
        if self.debug:

            # Run the processing step:
            self.run_step()

            # Synchronize:
            self._sync()
        else:

            # Run the processing step:
            catch_exception(self.run_step, self.log_info)

            # Synchronize:
            catch_exception(self._sync, self.log_info)