Declarativa Declarativa
 

 

Remote predicate calls

Miguel Calejo

Technical Note DI/UNL, October 1989
preliminary draft

This note adresses the following problem:

Given a Prolog program distributed in disjunct parts among N Prolog processes, to provide a simple mechanism for evaluation of any goal in a process which relies on (remote) subgoals evaluated by any of the other processes .

Two applications motivated this:

Having two Prologs with different sets of builtins use each other's abilities to solve goals, by distributing a program among both. Typically, one Prolog has all the interface builtins (MacLogic/C-Prolog), and the other has more computational power, the user program but nearly no interface builtins (IM).

Computing goals in a Prolog processor while using local information from other processors. For example, such may be the case in the future for a network of Macintoshes, each having an IM instance running on it.

This framework is inspired on Alegria's remote goal posting proposal [Alegria et al.], and may be seen as a particular concretization of his sketched protocol.

Here we're specifically concerned with the problem of having "Prolog agents" ask each other to solve goals, allowing recursive requests to solve subgoals. We're interested in a minimal and reasonably efficient solution, involving no significative overhead or implementation complexity. We start by incrementally building a schema for a pair of processes, and conclude generalizing to N processes.

1. Binary distributed coroutining

We'll define a "top-down constructive coroutining" scheme for Prolog computations, where a subgoal can be evaluated in a computation environment (coroutine) different from the one of its father. Consider that we have two coroutines and 2 Prolog primitives for communication:

send(T)

Sends a copy of term T to the other coroutine and succeeds, continuing execution.

receive(T)

Suspends the coroutine until a term matching T is available from the other coroutine. Bindings later performed in T do not propagate to the sender coroutine. Terms arrive assynchronously and transparently, and are kept in a FIFO queue.

We'll also assume that both coroutines are up and running (check below).

1.1. Deterministic remote goals

Let's remember an existing schema for deterministic goals. Goals to be evaluated locally are computed as usually. Goals to be evaluated at the "remote coroutine" environment must be computed with a metapredicate remote:
remote(G) :- send( call(G) ), receive(T), remote2(T,G).
remote2( answer(G), G) :- !.
remote2( call(G), A ) :- !, G, send( answer(G) ), receive(T), remote2(T,A).

The second coroutine can start with the goal:- repeat, receive(T), remote2(T,_).

The first coroutine (say, C-Prolog) starts with the usual top level. Whenever it wishes to solve a goal G in the other one (say,the IM), it calls remote(G). The IM may solve goals in C-Prolog by calling remote(G) as well.

Unfortunately, execution of cuts on backtracking can cause dessynchronization - i.e one computation being ready to give an additional answer to a call which no longer exists, because it was skipped over by some cut. We'll return to this problem after the next section.

1.2. Nondeterministic goals

To allow non-deterministic goals we must make sure both computations go forward and backward in sync. Here's a tentative solution:(******)
remote(G) :- send( call(G) ), receive(T), remote2(T,G).
remote2( answer(G), G).
remote2( answer(_), A) :- !, send( redo ), receive(T), remote2(T,A).
remote2( call(G), A ) :- fail_port, G, send( answer(G) ), receive(T), remote2(T,A).
fail_port.
fail_port :-
send( fail ), !, fail. % send anything _ answer(_) or call(_)…

1.3. Computations with cuts, part I

In order to synchronize both computations, on backtracking to a remote call we may have to backtrack further in the remote derivation. To each solution computed remotely we associate a mark (ID in the code below), and use it to later jump back.
remote(G) :- !, send( call(G) ), receive(T), remote2(T,G).
remote2( answer(_,G), G).
remote2( answer(ID,_), A) :- !, send( redo_to(ID) ), receive(T), remote2(T,A).
remote2( redo_to(ID), _ ) :- !, fail_back_to(ID).
remote2( call(G), A ) :- !, fail_port, G, this_place(ID), send( answer(ID,G) ), receive(T), remote2(T,A).
fail_port.
fail_port :- send( fail ), !, fail.

If a cut in one of the computations (C1) causes several "remote(G)" calls to be skipped on backtracking, the other computation C2 is nevertheless forced to backtrack to the appropriate point, as soon as C1 backtracks until an (older) remote(G) call. If the subcomputations in C2 (corresponding to the skipped remote(G) calls) included remote calls themselves (to C1), the corresponding subcomputations in C1 are skipped automatically: they are under the aforementioned remote(G) calls in C1.

The two complementary "jumping" primitives, fail_back_to(ID) and this_place(ID), can be implemented using a pair goal_number/retry, or a generalized "cut" primitive. For example, ALPES C-Prolog provides an "ancestor cut" - a cut which cuts up to a specified ancestor of the clause with the cut. A more general version of cut is available with YAP - removing choicepoints up to any previous goal in the derivation, not necessarily an ancestor.

Using YAP:
this_place(ID) :- ID is local_sp. % Current choice pointthis_place(_) :- fail . % to force creation of a choice point
fail_back_to(ID) :- '$cut_by'(ID), fail. % commit and fail

A clumsier and less efficient implementation could possibly make without these jumping primitives, by simulating them on backtracking to remote. However, there's another related problem.

1.4. Cuts, part II: "Disjunctive garbage"

There's one problem left: the computations may remain with "dangling OR" branches - when one of the computations is not forced back beyond a call which the other one no longer cares about, and instead goes strictly forward to satisfy later call requests. It does not affect the results of computations, it's just a space inefficiency issue.

This "dangling OR" situation is identical to when an user enters "break" mode in a conventional Prolog tracer and decides to never go back; or when an user asks more solutions to an incremental Prolog top level but never lets his original goal fail completely, while adding more constraints to his (incremental) query.

Ensuring absence of "dangling ORs" seems to involve either of:

Having the low-level implementation of cuts sending messages to the other process whenever a remote call is skipped during backtracking

 Implementing an explicit trail-like mechanism to later check, at the call port of any remote call, whether the other process needs extra synchronization (i.e., prunning of dangling ORs)

The first involves changes in the implementation of cuts. The second needs some means to distinguish active remote calls (in the current derivation) from those which have been skipped - again a solution involving either global overhead or a low-level implementation (to check whether there's an active choice point for a certain remote call).

Time to be pragmatic, and adopt two directives:

 If possible, make sure that the first remote call to the other process is failed explicitly (i.e., it is not skipped over by a cut)

Use an additional primitive, flush_remote, to initialize the remote coroutine, making it forget its current computation whenever desired.

Following are the changes to the definitions above. First, the new definition:flush_remote :- send(flush).

An extra clause is added to remote2:remote2(flush) :- !, $start(ID), fail_back_to(ID).

And the coroutining creation goal becomes::- repeat,
this_place(ID), assert($start(ID)),
    receive(T), remote2(T,_).

We'll now generalize to N coroutines instead of just two.

2. Distributed coroutining

We conjecture that the same scheme applies for more than 2 coroutines, since only one of them is active in turn, and global execution is essentially mimicking what would be an ordinary local Prolog execution. It's just necessary to make explicit names for processes.

The communication primitives should be:

send(D,T)

Sends a copy of term T to the destinatary coroutine D and succeeds.

receive(T,S)

Suspends the coroutine until a term is available from another coroutine. S is bound to the name of the sender coroutine. Bindings later performed in T do not propagate to the sender coroutine. Terms arrive assynchronously and transparently, and are kept in a FIFO queue.

And now the definitions for remote, etc.:
remote(D,G) :- !,
send( D, call(G) ), receive(T,S), remote2(T,S,G).
remote2( answer(_,G), S, G).
remote2( answer(ID,_), S, A) :- !, send( S,redo_to(ID) ), receive(T,S2), remote2(T,S2,A).
remote2( redo_to(ID),S,_ ) :- !, fail_back_to(ID).
remote2( call(G), S, A ) :- !, fail_port(S), G, this_place(ID), send( S,answer(ID,G) ), receive(T,S2), remote2(T,S2,A).
remote2(flush) :- $start(ID), fail_back_to(ID).
fail_port(_).
fail_port(S) :- send( S, fail ), !, fail.
flush_remote(P) :- send(P,flush).

The coroutine creation goal :
:- repeat,
this_place(ID), assert($start(ID)),
    receive(T,S), remote2(T,S,_).

References

J. Alegria, A. Dias and L. Caires, "Towards Distributed Tools for Heterogeneous Logic Programming Environments" in Proceedings of the 6th International Conference on Logic Programming (eds. Levi and Martelli), MIT Press, , 1989.

L.M. Pereira and R. Nasr, "Delta-Prolog: a distributed logic programming language" in Proc. International Conference on Fifth Generation Computer Systems , ICOT, ICOT, Tokyo, 283-291, 1984.


 Declarativa - Serviços de Informática, Lda.
  www.declarativa.com, info@declarativa.com  fax: +351-22-610 9574  tel: +351-22-610 9516
*Sede social (correio):
R. Cerca 88 4150-200 Porto Portugal
 *Centro de Desenvolvimento:
UPTEC - Parque de Ciência e Tecnologia da Universidade do Porto
Rua Actor Ferreira da Silva 100 4200-298 Porto Portugal