It’s been a week and a half since google-melange anounced the accepted student for google summer of code. I was luckcy enough to be accepted by Theano – sub-foundation of Python organization, to help them add new features: Allow user to modify compiled function. As scheduled, from 27th April to 23rd May is the community bounding period. During these days I ought to get familiar with Theano core code and Theano dev community.
Before the application started, I’ve dived into Theano cored and got the basic idea of what I’am going to do. However, to make the idea more clear and to fit the requirement that student should post every week about their progress. I decide to write two post about Theano core – about how theano work. This is the first post. This post will talk about what is a function? And how a function is generate.
How a function is generated?
Just recall how we compiled a function func = theano.function( [ inputs ], output ), we can know that we should start out journey from method function(), which locates in theano/compile/founction.py.
In method function(), after some data verification, it will call orig_func() or pfunc() which return a function that user will get. Since pfunc() will also call orig_func(), we are going to look into pfunc() first.
pfunc.py
pfunc() have two major tasks:
Transfer input_variable into In() instances. So does shared_variable. ( In function graph, SharedVariabls are treated as input, updates are treated as output ).
Rebuild computational graph using updates and inputs, transform output into Out instances.
defpfunc(...somearguments...):# Clones the replacements named in the givens argument, and points each Var1 to# the clone of Var2.# Transform params into theano.compile.In objects.inputs=[_pfunc_param_to_in(p,allow_downcast=allow_input_downcast)forpinparams]# clones the outputs and the update expressions. This rebuilds a computation graph# from the inputs and the givens. ( Irrelative to me. Pass )output_vars=rebuild_collect_shared(outputs,in_variables,replace=givens,updates=updates,rebuild_strict=rebuild_strict,copy_inputs_over=True,no_default_updates=no_default_updates)# extracting the argumentsinput_variables,cloned_outputs,other_stuff=output_varsclone_d,update_d,update_expr,shared_inputs=other_stufffori,ivinzip(inputs,input_variables):i.variable=ivforsvinshared_inputs:# pass value of None here# value will be stored in the resulting functions' defaults list# but since the value of shared variables never needs to be refed, it is not neededifsvinupdate_d:si=In(variable=sv,value=sv.container,mutable=True,borrow=True,update=update_d[sv],shared=True)else:si=In(variable=sv,value=sv.container,mutable=False,borrow=True,shared=True)inputs.append(si)returnorig_function(inputs,cloned_outputs,mode,accept_inplace=accept_inplace,name=name,profile=profile,on_unused_input=on_unused_input,output_keys=output_keys
orig_func():
Now it time for a look into orig_func(). orig_func() will again makes sure that inputs and outputs are transformed into In an Out. And then it will use create method in FunctionMaker to make a function, which will be return.
1234567891011121314151617181920212223
deforig_function(inputs,outputs,mode=None,accept_inplace=False,name=None,profile=None,on_unused_input=None,output_keys=None):# conver input variable into instances of In() instancesinputs=map(convert_function_input,inputs)# so do outputsifoutputsisnotNone:ifisinstance(outputs,(list,tuple)):outputs=map(FunctionMaker.wrap_out,outputs)else:outputs=FunctionMaker.wrap_out(outputs)# In()s and Out()s will be passed into FunctionMaker and a function will be create from it by calling create() method.fn=Maker(inputs,outputs,mode,accept_inplace=accept_inplace,profile=profile,on_unused_input=on_unused_input,output_keys=output_keys).create(defaults)# ^^^^
FunctionMaker:
FunctionMaker.__init()__ is where fgraph is extracted and optimized. FuncitonMaker.create() is where function will be compiled and linked. In fact, FunctionMaker.linker.make_thunk() is where function is linked.
classFunctionMaker:def__init__(...args...):# again, make sure that input/output are tranformed into In and Outinputs,outputs=map(self.wrap_in,inputs),map(self.wrap_out,outputs)# ???_inputs=gof.graph.inputs([o.variableforoinoutputs]+[i.updateforiininputsifgetattr(i,'update',False)])# make indices ... which is uselessindices=[[input]+self.expand_in(input,_inputs)forinputininputs]# get fgraphiffgraphisNode:fgraph,additional_outputs=std_fgraph(inputs,outputs,accept_inplace)else:_,additional_outputs=std_fgraph(inputs,outputs,accept_inplace)self.fgraph=fgraph# fetch optimizor and linkeroptimizer,linker=mode.optimizer,copy.copy(mode.linker)# optimize fgraph# if need_opt:# optimization code here# linker accept fgraph ifno_borrow:self.linker=linker.accept(fgraph,no_recycling=infer_reuse_pattern(fgraph,no_borrow))else:self.linker=linker.accept(fgraph)ifhasattr(linker,'accept_var_updates'):# hacky thing so VMLinker knows about updatesself.linker.accept_var_updates(fgraph_updated_vars(fgraph,inputs))# some other configeration here# ...defcreate(self,input_storage=None,trustme=False):""" Create a function. Input storage is the 'value' attribute of each In instances """# construct input_storage_list and default listfori,((input,indices,subinputs),input_storage_i)inenumerate(zip(self.indices,input_storage)):# a lot of codes here.""" Q: What's indices? List of (SymbolicInput, incide, [SymbolicInput..]). The first one is the input vaiable; the incide is the index of the input in the input list; the [SymIn...] the relevant input(?); According to the document, the last two is deprecated. So it can be regarded as list of SymbolicInput. Q: What's defaults? A: List of 3-tuples. Each tuple corresponds to one input_storage. ( Bool: Is this input required at each function call?, Bool: Should this inputs value be reverted to default value after each call? AnyType: The value(storage) associated with this input. ) """# call make_thunk() from linker and get fntry:theano.config.traceback.limit=0_fn,_i,_o=self.linker.make_thunk(input_storage=input_storage_lists)# ^ ^ ^ => (function, input_containers, output_containers)# where function is a thunk that operates on the returned variables.# Because the map_storag() in make_thunk()# from here on, the input/output_storage represent all I/O of all relative nodes# ALso, storage is container, Instead of SymbolicKitfinally:theano.config.traceback.limit=limit_orig# get a function, here function_builder() is the constructor# of class Function.fn=self.function_builder(_fn,_i,_o,self.indices,self.outputs,defaults,self.unpack_single,self.return_none,self.output_keys,self)returnfn
What’s theano.function?
Each function is a callable object. theano.function is not a python function. Instead, it a class with method __call__(). Every funciton stores its own fgraph, maker, storages and many other configurations. However, the core of a function is a function fn() returned by linker.make_thunk(). Every time a function is called, it will first verify the input data, and then call self.fn() to get output values.
Now I know how is a function borned. Also, I know that to complete my missions, I need to focus more on FunctionMaker and Function rather that orig_func() and pfunc(). However, there still exist some question, such as: What does make_thunk() do? and What is In(), Out() and container? In the next post, I will have a look at this and other relative data structures.