fair-acc / gnuradio4

Prototype implementations for a more compile-time efficient flowgraph API
GNU Lesser General Public License v3.0
22 stars 8 forks source link

Semantic of the Sync and Async Ports #369

Open drslebedev opened 2 weeks ago

drslebedev commented 2 weeks ago

With this issue we want to review the semantic of Async ports and clearly define the difference between Sync and Async cases.

Number of samples constraints

There are two types of constraints used to calculate the input and output number of samples to be processed:

  1. Port Constraints: Min/max number of samples set individually for each port.
  2. Resampling Constraints: Numerator, denominator, and stride set for each block.

Sync and Async Port Behaviors

Sync Ports:

Async Ports:

Examples

  1. 4Sync + 1 Async Inputs -> 1 Sync Output:

    • All 4 Sync inputs have consistent input (same number of samples) and Async has some input samples (fulfilling port constraints) -> call processBulk.
    • All 4 Sync inputs have consistent input (fulfilling port + resampling constraints), Async has no input (or input does not fulfill port constraints) -> call processBulk (Async port has no input samples).
    • Samples do not fulfill Sync input constraints, Async has input (fulfilling port constraints) -> call processBulk (no samples for input Sync ports, no samples for output Sync port, only samples for input Async port).
    • No Sync input available and no Async data -> do not call processBulk.
  2. Sync and/or Async Inputs -> 4 Sync + 1 Async Outputs:

    • If at least one Async output port is present, always call processBulk (unless Async output port constraints are not met). This is because the user can call output.publish() even if no input samples are present. Note: This can lead to performance issues due to busy polling. processBulk can be invoked upto 4G/s.
    • Consider introducing a policy to prevent this and only call processBulk for Async ports if input samples are available. This can be a port policy.

consume() / publish() Only for Async Ports

TODO

daniestevez commented 1 week ago

The processBulk function is called independently of the available samples on other Sync and/or Async ports.

Can you clarify when processBulk is called and when it isn't called? For example, for a block with Sync input and Async output, is processBulk called if there are zero items available in the input?

The user can change the sample ratio iteration-to-iteration using input.consume() and output.publish()

Will this stop being true for Sync ports? The kind of use case that I have in mind for a Sync input and Sync output block that sometimes doesn't respect its M:N ratio is a block like the GR 3.10 Symbol Sync. Usually it decimates by a fixed ratio (the GR 3.10 block allows this ratio to be a float, but for the sake of the example let's suppose the ratio is a rational number). But the decimation is done in terms of a closed loop that depends on the values of the input, so sometimes the block "slips" one input sample forward or backwards by consuming one more or one less samples than the nominal ratio would indicate.

Reading the section on "consume() / publish() Only for Async Ports", it seems that this will be the case. In this case it might perhaps be necessary to have a way for blocks with Async ports to indicate a rough input_samples/ouput_samples relation.

Specially important in this case is to clarify what happens with busy polling a processBulk() function for a block with only Async ports. If consume() and publish() are removed for Sync ports, then every block that doesn't have a definite M:N input/output relation needs to be Async-only, but most of this blocks need some input to produce some output.

drslebedev commented 1 week ago

If at least one Async output port is present, processBulk is always called, even if no input samples are available (unless Async output port constraints are not met, namely min/max port constraints).

We consider introducing a policy to prevent busy polling and only call processBulk for Async ports if input samples are available.

Will this stop being true for Sync ports?

Yes, because users can easily ignoring N:M ratio using consume()/publish(). If the ratio is not fixed one should use Async ports.

daniestevez commented 1 week ago

Thanks for the clarifications. What I don't understand is, for a block with a single input port and a single output port, which are the differences between the cases:

Looking at the list of differences between Sync and Async ports, I see that one of them is that if there is one Async port then processBulk() is called continuously (this isn't really a port-specific property, but a block-specific property), and that N:M Resampling is not taken into account (this isn't really a property of a single port, but a property that relates an input port and an output port). Maybe these all three cases behave the same? Maybe only the Async input, Async output makes sense? The Async annotation is applied at the level of ports, but to me it seems that its semantics affect properties that belong to a larger construct than just a single port.

drslebedev commented 1 week ago

In general, these three cases behave similarly, but there are some differences to note.

The main differences between these cases lie in how constraints are applied:

For the output port:

Additionally, for an Async port, a user can call consume() and publish().

daniestevez commented 1 week ago

For a block with a single Async input and a single Sync output, does processBulk() only get called if the input has some samples available? Wouldn't this kind of block ought to behave as a source block (so its processBulk() always gets called) which has an additional input that may or may not have samples at any point?

drslebedev commented 1 week ago

For the output Sync port, the processBulk function is only invoked when input samples are available. Otherwise, it's unclear how many samples to provide for the Sync output port, as Sync ports do not support a publish() call.

For a Sourceblock with an additional Async input, it seems that both the input and output need to be Async.