master/worker communication using pc.submit()

Using the Multiple Run Fitter, praxis, etc..
Post Reply
Brad

master/worker communication using pc.submit()

Post by Brad »

I've written a little test program to teach myself the basics of using the ParallelContext Class. For the demo, a spherical patch is created with the "pas" mechanism. A current clamp provides the "stimulus" and voltage is recorded. The made-up experiment I want to run is to test a range of current injection amplitudes with random start times. I'd like to record the raw output voltage waveforms as well as gather the maximum depolarization values for each amplitude (since in this simple exercise we're only varying the delay within a single task, we don't expect the max amp to change from sweep to sweep).

My strategy is to divide the tasks up according to current injection amplitude so that each task is assigned a single amplitude to test with a specified number of repeats. Each repeat generates a random number from a normal distribution and sets that to the stimulation delay. The voltage output is collected into a matrix and the max value is tested. Thus each task posted to the bulletin board does the following:

1. saves a unique data file with data for a specific amp
2. returns the max depol to the master process

#1 is pretty straight-forward; it should work as long as there isn't a conflict over file access (and that shouldn't happen if we handle our files correctly). #2 is where using the PC class requires some special attention.

The way the demo works, is that each task packs its IClamp amplitude and max depolarization into a message, which is then posted with key value grabbed from hoc_ac_. The master task then pulls the message and unpacks the two values.

In case this might be of use to anybody else, I've included a full listing.

Code: Select all

// test ParallelContext class

/////////////////////////////
// file/directory handling; loading standard run library

strdef homedir, libdir, nrndir, buffdir
homedir = getcwd()
nrndir = neuronhome()

if (unix_mac_pc() == 3) {
	sprint(buffdir,"%s\\lib\\hoc\\",nrndir)
} else {
	sprint(buffdir,"%s/lib/hoc/",nrndir)
}
chdir(buffdir)
xopen("stdrun.hoc")
chdir(homedir)



/////////////////////////////
// parameters

TSTOP = 1000		// msec
DT = 0.1		// msec
V_INIT = -70		// mV
integrator = 1		// 0 -> fixed time step; 1 -> adaptive time step
ATOL = 0.01
MEAN = 0		// mean rand IClamp delay (msec)
VARIANCE = 5		// variance rand IClamp delay (msec)
REPEATS = 3
SEED = TSTOP/1000+ATOL+MEAN+VARIANCE-REPEATS	// seed for RNG

AMP_START = 0.01 	// IClamp starting amp (nA)
AMP_STOP = 0.03
AMP_STEP = 0.01



/////////////////////////////
// simulation setup

objref cvode
cvode = new CVode()
cvode.active(integrator)
cvode.atol(ATOL)



/////////////////////////////
// cell setup

create patch
objref recsec

forall {
	L = 10
	diam = 10
	nseg = 1

	insert pas
	Ra = 150
	e_pas = V_INIT
}

patch	recsec = new SectionRef()



/////////////////////////////
// electrophys setup

objref stim
recsec.sec stim = new IClamp(0.5)
stim.del = TSTOP/2
stim.dur = 10
stim.amp = 0



/////////////////////////////
// setup simulation run

objref xeven, yeven, tvec, vvec
xeven = new Vector()
xeven.indgen(0,TSTOP,DT)

obfunc run_simulation() {
	steps_per_ms = 1/DT
	dt = DT
	tstop = TSTOP
	v_init = V_INIT

	tvec = new Vector()
	vvec = new Vector()
	recsec.sec cvode.record(&v(0.5),vvec,tvec)

	yeven = new Vector(xeven.size())

	run()	// calls stdrun library procedure; includes standard init() procedure

	yeven.interpolate(xeven,tvec,vvec)	// generate vector of data points with evenly-spaced time intervals
	
	return yeven
}



/////////////////////////////
// setup ParallelContext

objref pc
pc = new ParallelContext()

print "number of hosts: ", pc.nhost(), "\thost id: ", pc.id()

objref rand, data_buffer, data_matrix, file
strdef filename
data_matrix = new Matrix(TSTOP/DT+1,REPEATS+1)	// 0th column is time vector; the ith data vector goes into the ith+1 column
data_buffer = new Vector()
rand = new Random(SEED)
rand.normal(MEAN,VARIANCE)

func main() { local key, i, num_repeats, amplitude, MX

	MX = -1e9

	key = hoc_ac_
	num_repeats = $1
	amplitude = $2

	print "host: ", pc.id(), "\tkey: ", key, "\trepeats: ", num_repeats, "\tamplitude: ", amplitude

	stim.amp = amplitude

	for i = 0,num_repeats-1 {
		stim.del = TSTOP/2 + rand.repick()
		data_buffer = new Vector()
		data_buffer = run_simulation()
		data_matrix.setcol(i+1,data_buffer)

		if (data_buffer.max() > MX) {
			MX = data_buffer.max()
		}
	}

	data_matrix.setcol(0,xeven)	// put time vector into 0th column

	sprint(filename,"%f.data",amplitude)
	file = new File()
	file.wopen(filename)
	data_matrix.fprint(0,file,"%f\t","\n")
	file.close()

	pc.pack(amplitude)
	pc.pack(MX)
	pc.post(key)

	return key
}



/////////////////////////////
// perform parallel simulations

pc.runworker()

objref z, maxV
z = new Vector()
z.indgen(AMP_START,AMP_STOP,AMP_STEP)
maxV = new Vector(z.size())

for i = 0,z.size()-1 {
	pc.submit("main",REPEATS,z.x[i])
}



while ((id = pc.working()) != 0) {
	pc.look_take(id)
	
	amp = pc.upkscalar()
	mx = pc.upkscalar()

	index = z.indwhere("==",amp)
	maxV.x[index] = mx
}

pc.done()

print "max depols: "
maxV.printf()
Alternatives
It seems it is generally a good idea to avoid using hoc_ac_ because of its volatility, but the demo seems to work properly. From the documentation, it appears that using pc.submit with an explicit userid *excludes* the ability to unpack input arguments, and this was a restriction that I don't want to impose if I can avoid it. (To quote the documentation, "If there is no explicit userid, then the args (after the function name) are saved locally and can be unpacked when the corresponding working call returns" implies to me that if there is an explicit userid, than this cannot be done.) The indwhere() statement is a little ugly and I'd like to achieve the same result as using a userid with pc.submit, but without the restriction. I can change the FOR and WHILE loops like this to explicitly supply what is in effect a userid as the first argument to the main() procedure:

Code: Select all

func main() { local key, i, num_repeats, amplitude, MX

	MX = -1e9

	key = $1
	num_repeats = $2
	amplitude = $3

...

	pc.pack(amplitude)
	pc.pack(MX)
	pc.post(key)

	return key
}

for i = 0,z.size()-1 {
	pc.submit("main",i,REPEATS,z.x[i])
}



while (pc.working()) {
	key = pc.retval()
	pc.look_take(key)
	
	amp = pc.upkscalar()
	mx = pc.upkscalar()

	maxV.x[key] = mx
}
Questions:
1.) For what I'm trying to do here, are there any subtle (or maybe not subtle) issues that make one method of master/worker communications superior to the other? There seem to be several ways to do this communication and it's not clear to me when one method should be preferred over another.

2.) What benefit is there to supplying an explicit userid to pc.submit other than the fact that one can get its value with a call to pc.userid(), given that doing so might restrict unpacking arguments and the fact that one can get nearly the same effect by using an additional argument to supply a "userid"? Also please note that I have attempted to avoid writing to "global" variables from within the task functions and my task doesn't submit child tasks, so I should avoid the issues discussed in the pc.working() documentation.

In the end, it seems that the safest, most powerful, and efficient strategy is to supply a "userid" as an argument to the task function: it has almost all of the benefits of having a userid (except that it can't be referenced by pc.userid()), doesn't restrict packing/unpacking arguments, and allows easier "dereferencing" in the pc.working() loop at a cost of a minimal amount of overhead.
hines
Site Admin
Posts: 1711
Joined: Wed May 18, 2005 3:32 pm

Post by hines »

In the end, it seems that the safest, most powerful, and efficient strategy is to supply a "userid" as an argument to the task function
I agree with this and would only call it
a taskid.

The only advice I would add is in respect the the Random usage. Given the possibility of parallel bugs I feel it is best to write the code so that the result of any task is independent of what machine it runs on or what ran on that machine earlier in the global run. Also if one uses
a random variable you would like the stream to be both repeatable for any task and statistically independent of all the other tasks. Thus I would use the
http://www.neuron.yale.edu/neuron/stati ... #MCellRan4
and call it for every task with a highindex which was a large multiple of the taskid.
If one wants a statistically independent run then just change the
http://www.neuron.yale.edu/neuron/stati ... _ran4_init
Post Reply