See: https://issues.apache.org/jira/browse/FLINK-3002
On Wed, Nov 11, 2015 at 10:54 AM, Stephan Ewen <se...@apache.org> wrote: > "Either" an "Optional" types are quite useful. > > Let's add them to the core Java API. > > On Wed, Nov 11, 2015 at 10:00 AM, Vasiliki Kalavri < > vasilikikala...@gmail.com> wrote: > >> Thanks Fabian! I'll try that :) >> >> On 10 November 2015 at 22:31, Fabian Hueske <fhue...@gmail.com> wrote: >> >> > You could implement a Java Either type (similar to Scala's Either) that >> > either has a Message or the VertexState and a corresponding >> TypeInformation >> > and TypeSerializer that serializes a byte flag to indicate which both >> types >> > is used. >> > It might actually make sense, to add a generic Either type to the Java >> API >> > in general (similar to the Java Tuples with resemble the Scala Tuples). >> > >> > Cheers, Fabian >> > >> > 2015-11-10 22:16 GMT+01:00 Vasiliki Kalavri <vasilikikala...@gmail.com >> >: >> > >> > > Hi, >> > > >> > > after running a few experiments, I can confirm that putting the >> combiner >> > > after the flatMap is indeed more efficient. >> > > >> > > I ran SSSP and Connected Components with Spargel, GSA, and the Pregel >> > model >> > > and the results are the following: >> > > >> > > - for SSSP, Spargel is always the slowest, GSA is a ~1.2x faster and >> > Pregel >> > > is ~1.1x faster without combiner, ~1.3x faster with combiner. >> > > - for Connected Components, Spargel and GSA perform similarly, while >> > Pregel >> > > is 1.4-1.6x slower. >> > > >> > > To start with, this is much better than I expected :) >> > > However, there is a main shortcoming in my current implementation that >> > > negatively impacts performance: >> > > Since the compute function coGroup needs to output both new vertex >> values >> > > and new messages, I emit a wrapping tuple that contains both vertex >> state >> > > and messages and then filter them out based on a boolean field. The >> > problem >> > > is that since I cannot emit null fields, I emit a dummy message for >> each >> > > new vertex state and a dummy vertex state for each new message. That >> > > essentially means that the intermediate messages result is double in >> > size, >> > > if say the vertex values are of the same type as the messages (can be >> > worse >> > > if the vertex values are more complex). >> > > So my question is, is there a way to avoid this redundancy, by either >> > > emitting null fields or by creating an operator that could emit 2 >> > different >> > > types of tuples? >> > > >> > > Thanks! >> > > -Vasia. >> > > >> > > On 9 November 2015 at 15:20, Fabian Hueske <fhue...@gmail.com> wrote: >> > > >> > > > Hi Vasia, >> > > > >> > > > sorry for the late reply. >> > > > I don't think there is a big difference. In both cases, the >> > partitioning >> > > > and sorting happens at the end of the iteration. >> > > > If the groupReduce is applied before the workset is returned, the >> > sorting >> > > > happens on the filtered result (after the flatMap) which might be a >> > > little >> > > > bit more efficient (depending on the ratio of messages and solution >> set >> > > > updates). Also it does not require that the initial workset is >> sorted >> > for >> > > > the first groupReduce. >> > > > >> > > > I would put it at the end. >> > > > >> > > > Cheers, Fabian >> > > > >> > > > 2015-11-05 17:19 GMT+01:00 Vasiliki Kalavri < >> vasilikikala...@gmail.com >> > >: >> > > > >> > > > > @Fabian >> > > > > >> > > > > Is there any advantage in putting the reducer-combiner before >> > updating >> > > > the >> > > > > workset vs. after (i.e. right before the join with the solution >> set)? >> > > > > >> > > > > If it helps, here are the plans of these 2 alternatives: >> > > > > >> > > > > >> > > > > >> > > > >> > > >> > >> https://drive.google.com/file/d/0BzQJrI2eGlyYcFV2RFo5dUFNXzg/view?usp=sharing >> > > > > >> > > > > >> > > > >> > > >> > >> https://drive.google.com/file/d/0BzQJrI2eGlyYN014NXp6OEZUdGs/view?usp=sharing >> > > > > >> > > > > Thanks a lot for the help! >> > > > > >> > > > > -Vasia. >> > > > > >> > > > > On 30 October 2015 at 21:28, Fabian Hueske <fhue...@gmail.com> >> > wrote: >> > > > > >> > > > > > We can of course inject an optional ReduceFunction (or >> GroupReduce, >> > > or >> > > > > > combinable GroupReduce) to reduce the size of the work set. >> > > > > > I suggested to remove the GroupReduce function, because it did >> only >> > > > > collect >> > > > > > all messages into a single record by emitting the input iterator >> > > which >> > > > is >> > > > > > quite dangerous. Applying a combinable reduce function is could >> > > improve >> > > > > the >> > > > > > performance considerably. >> > > > > > >> > > > > > The good news is that it would come "for free" because the >> > necessary >> > > > > > partitioning and sorting can be reused (given the forwardField >> > > > > annotations >> > > > > > are correctly set): >> > > > > > - The partitioning of the reduce can be reused for the join with >> > the >> > > > > > solution set >> > > > > > - The sort of the reduce is preserved by the join with the >> > in-memory >> > > > > > hash-table of the solution set and can be reused for the >> coGroup. >> > > > > > >> > > > > > Best, >> > > > > > Fabian >> > > > > > >> > > > > > 2015-10-30 18:38 GMT+01:00 Vasiliki Kalavri < >> > > vasilikikala...@gmail.com >> > > > >: >> > > > > > >> > > > > > > Hi Fabian, >> > > > > > > >> > > > > > > thanks so much for looking into this so quickly :-) >> > > > > > > >> > > > > > > One update I have to make is that I tried running a few >> > experiments >> > > > > with >> > > > > > > this on a 6-node cluster. The current implementation gets >> stuck >> > at >> > > > > > > "Rebuilding Workset Properties" and never finishes a single >> > > > iteration. >> > > > > > > Running the plan of one superstep without a delta iteration >> > > > terminates >> > > > > > > fine. I didn't have access to the cluster today, so I couldn't >> > > debug >> > > > > this >> > > > > > > further, but I will do as soon as I have access again. >> > > > > > > >> > > > > > > The rest of my comments are inline: >> > > > > > > >> > > > > > > On 30 October 2015 at 17:53, Fabian Hueske <fhue...@gmail.com >> > >> > > > wrote: >> > > > > > > >> > > > > > > > Hi Vasia, >> > > > > > > > >> > > > > > > > I had a look at your new implementation and have a few ideas >> > for >> > > > > > > > improvements. >> > > > > > > > 1) Sending out the input iterator as you do in the last >> > > GroupReduce >> > > > > is >> > > > > > > > quite dangerous and does not give a benefit compared to >> > > collecting >> > > > > all >> > > > > > > > elements. Even though it is an iterator, it needs to be >> > > completely >> > > > > > > > materialized in-memory whenever the record is touched by >> Flink >> > or >> > > > > user >> > > > > > > > code. >> > > > > > > > I would propose to skip the reduce step completely and >> handle >> > all >> > > > > > > messages >> > > > > > > > separates and only collect them in the CoGroup function >> before >> > > > giving >> > > > > > > them >> > > > > > > > into the VertexComputeFunction. Be careful, to only do that >> > with >> > > > > > > > objectReuse disabled or take care to properly copy the >> > messages. >> > > If >> > > > > you >> > > > > > > > collect the messages in the CoGroup, you don't need the >> > > > GroupReduce, >> > > > > > have >> > > > > > > > smaller records and you can remove the MessageIterator class >> > > > > > completely. >> > > > > > > > >> > > > > > > >> > > > > > > I see. The idea was to expose to message combiner that user >> > could >> > > > > > > implement if the messages are combinable, e.g. min, sum. This >> > is a >> > > > > > common >> > > > > > > case and reduces the message load significantly. Is there a >> way I >> > > > could >> > > > > > do >> > > > > > > something similar before the coGroup? >> > > > > > > >> > > > > > > >> > > > > > > >> > > > > > > > 2) Add this annotation to the AppendVertexState function: >> > > > > > > > @ForwardedFieldsFirst("*->f0"). This indicates that the >> > complete >> > > > > > element >> > > > > > > of >> > > > > > > > the first input becomes the first field of the output. Since >> > the >> > > > > input >> > > > > > is >> > > > > > > > partitioned on "f0" (it comes out of the partitioned >> solution >> > > set) >> > > > > the >> > > > > > > > result of ApplyVertexState will be partitioned on "f0.f0" >> which >> > > is >> > > > > > > > (accidentially :-D) the join key of the following coGroup >> > > function >> > > > -> >> > > > > > no >> > > > > > > > partitioning :-) >> > > > > > > > >> > > > > > > >> > > > > > > Great! I totally missed that ;) >> > > > > > > >> > > > > > > >> > > > > > > >> > > > > > > > 3) Adding the two flatMap functions behind the CoGroup >> prevents >> > > > > > chaining >> > > > > > > > and causes therefore some serialization overhead but >> shouldn't >> > be >> > > > too >> > > > > > > bad. >> > > > > > > > >> > > > > > > > So in total I would make this program as follows: >> > > > > > > > >> > > > > > > > iVertices<K,VV> >> > > > > > > > iMessage<K, Message> = iVertices.map(new InitWorkSet()); >> > > > > > > > >> > > > > > > > iteration = iVertices.iterateDelta(iMessages, maxIt, 0) >> > > > > > > > verticesWithMessage<Vertex, Message> = >> > iteration.getSolutionSet() >> > > > > > > > .join(iteration.workSet()) >> > > > > > > > .where(0) // solution set is local and build side >> > > > > > > > .equalTo(0) // workset is shuffled and probe side of >> hashjoin >> > > > > > > > superstepComp<Vertex,Tuple2<K, Message>,Bool> = >> > > > > > > > verticesWithMessage.coGroup(edgessWithValue) >> > > > > > > > .where("f0.f0") // vwm is locally forward and sorted >> > > > > > > > .equalTo(0) // edges are already partitioned and sorted >> (if >> > > > cached >> > > > > > > > correctly) >> > > > > > > > .with(...) // The coGroup collects all messages in a >> > collection >> > > > and >> > > > > > > gives >> > > > > > > > it to the ComputeFunction >> > > > > > > > delta<Vertex> = superStepComp.flatMap(...) // partitioned >> when >> > > > merged >> > > > > > > into >> > > > > > > > solution set >> > > > > > > > workSet<K, Message> = superStepComp.flatMap(...) // >> partitioned >> > > for >> > > > > > join >> > > > > > > > iteration.closeWith(delta, workSet) >> > > > > > > > >> > > > > > > > So, if I am correct, the program will >> > > > > > > > - partition the workset >> > > > > > > > - sort the vertices with messages >> > > > > > > > - partition the delta >> > > > > > > > >> > > > > > > > One observation I have is that this program requires that >> all >> > > > > messages >> > > > > > > fit >> > > > > > > > into memory. Was that also the case before? >> > > > > > > > >> > > > > > > >> > > > > > > I believe not. The plan has one coGroup that produces the >> > messages >> > > > > and a >> > > > > > > following coGroup that groups by the messages "target ID" and >> > > > consumes >> > > > > > > them in an iterator. That doesn't require them to fit in >> memory, >> > > > > right? >> > > > > > > >> > > > > > > >> > > > > > > I'm also working on a version where the graph is represented >> as >> > an >> > > > > > > adjacency list, instead of two separate datasets of vertices >> and >> > > > edges. >> > > > > > The >> > > > > > > disadvantage is that the graph has to fit in memory, but I >> think >> > > the >> > > > > > > advantages are many. We'll be able to support edge value >> > updates, >> > > > edge >> > > > > > > mutations and different edge access order guarantees. I'll get >> > back >> > > > to >> > > > > > this >> > > > > > > thread when I have a working prototype. >> > > > > > > >> > > > > > > >> > > > > > > > >> > > > > > > > Cheers, >> > > > > > > > Fabian >> > > > > > > > >> > > > > > > >> > > > > > > Thanks again! >> > > > > > > >> > > > > > > Cheers, >> > > > > > > -Vasia. >> > > > > > > >> > > > > > > >> > > > > > > >> > > > > > > > >> > > > > > > > >> > > > > > > > 2015-10-27 19:10 GMT+01:00 Vasiliki Kalavri < >> > > > > vasilikikala...@gmail.com >> > > > > > >: >> > > > > > > > >> > > > > > > > > @Martin: thanks for your input! If you ran into any other >> > > issues >> > > > > > that I >> > > > > > > > > didn't mention, please let us know. Obviously, even with >> my >> > > > > proposal, >> > > > > > > > there >> > > > > > > > > are still features we cannot support, e.g. updating edge >> > values >> > > > and >> > > > > > > graph >> > > > > > > > > mutations. We'll need to re-think the underlying iteration >> > > and/or >> > > > > > graph >> > > > > > > > > representation for those. >> > > > > > > > > >> > > > > > > > > @Fabian: thanks a lot, no rush :) >> > > > > > > > > Let me give you some more information that might make it >> > easier >> > > > to >> > > > > > > reason >> > > > > > > > > about performance: >> > > > > > > > > >> > > > > > > > > Currently, in Spargel the SolutionSet (SS) keeps the >> vertex >> > > state >> > > > > and >> > > > > > > the >> > > > > > > > > workset (WS) keeps the active vertices. The iteration is >> > > composed >> > > > > of >> > > > > > 2 >> > > > > > > > > coGroups. The first one takes the WS and the edges and >> > produces >> > > > > > > messages. >> > > > > > > > > The second one takes the messages and the SS and produced >> the >> > > new >> > > > > WS >> > > > > > > and >> > > > > > > > > the SS-delta. >> > > > > > > > > >> > > > > > > > > In my proposal, the SS has the vertex state and the WS has >> > > > > <vertexId, >> > > > > > > > > MessageIterator> pairs, i.e. the inbox of each vertex. The >> > plan >> > > > is >> > > > > > more >> > > > > > > > > complicated because compute() needs to have two iterators: >> > over >> > > > the >> > > > > > > edges >> > > > > > > > > and over the messages. >> > > > > > > > > First, I join SS and WS to get the active vertices (have >> > > > received a >> > > > > > > msg) >> > > > > > > > > and their current state. Then I coGroup the result with >> the >> > > edges >> > > > > to >> > > > > > > > access >> > > > > > > > > the neighbors. Now the main problem is that this coGroup >> > needs >> > > to >> > > > > > have >> > > > > > > 2 >> > > > > > > > > outputs: the new messages and the new vertex value. I >> > couldn't >> > > > > really >> > > > > > > > find >> > > > > > > > > a nice way to do this, so I'm emitting a Tuple that >> contains >> > > both >> > > > > > types >> > > > > > > > and >> > > > > > > > > I have a flag to separate them later with 2 flatMaps. From >> > the >> > > > > vertex >> > > > > > > > > flatMap, I crete the SS-delta and from the messaged >> flatMap I >> > > > > apply a >> > > > > > > > > reduce to group the messages by vertex and send them to >> the >> > new >> > > > WS. >> > > > > > One >> > > > > > > > > optimization would be to expose a combiner here to reduce >> > > message >> > > > > > size. >> > > > > > > > > >> > > > > > > > > tl;dr: >> > > > > > > > > 1. 2 coGroups vs. Join + coGroup + flatMap + reduce >> > > > > > > > > 2. how can we efficiently emit 2 different types of >> records >> > > from >> > > > a >> > > > > > > > coGroup? >> > > > > > > > > 3. does it make any difference if we group/combine the >> > messages >> > > > > > before >> > > > > > > > > updating the workset or after? >> > > > > > > > > >> > > > > > > > > Cheers, >> > > > > > > > > -Vasia. >> > > > > > > > > >> > > > > > > > > >> > > > > > > > > On 27 October 2015 at 18:39, Fabian Hueske < >> > fhue...@gmail.com> >> > > > > > wrote: >> > > > > > > > > >> > > > > > > > > > I'll try to have a look at the proposal from a >> performance >> > > > point >> > > > > of >> > > > > > > > view >> > > > > > > > > in >> > > > > > > > > > the next days. >> > > > > > > > > > Please ping me, if I don't follow up this thread. >> > > > > > > > > > >> > > > > > > > > > Cheers, Fabian >> > > > > > > > > > >> > > > > > > > > > 2015-10-27 18:28 GMT+01:00 Martin Junghanns < >> > > > > > m.jungha...@mailbox.org >> > > > > > > >: >> > > > > > > > > > >> > > > > > > > > > > Hi, >> > > > > > > > > > > >> > > > > > > > > > > At our group, we also moved several algorithms from >> > Giraph >> > > to >> > > > > > Gelly >> > > > > > > > and >> > > > > > > > > > > ran into some confusing issues (first in >> understanding, >> > > > second >> > > > > > > during >> > > > > > > > > > > implementation) caused by the conceptional differences >> > you >> > > > > > > described. >> > > > > > > > > > > >> > > > > > > > > > > If there are no concrete advantages (performance >> mainly) >> > in >> > > > the >> > > > > > > > Spargel >> > > > > > > > > > > implementation, we would be very happy to see the >> Gelly >> > API >> > > > be >> > > > > > > > aligned >> > > > > > > > > to >> > > > > > > > > > > Pregel-like systems. >> > > > > > > > > > > >> > > > > > > > > > > Your SSSP example speaks for itself. Straightforward, >> if >> > > the >> > > > > > reader >> > > > > > > > is >> > > > > > > > > > > familiar with Pregel/Giraph/... >> > > > > > > > > > > >> > > > > > > > > > > Best, >> > > > > > > > > > > Martin >> > > > > > > > > > > >> > > > > > > > > > > >> > > > > > > > > > > On 27.10.2015 17:40, Vasiliki Kalavri wrote: >> > > > > > > > > > > >> > > > > > > > > > >> Hello squirrels, >> > > > > > > > > > >> >> > > > > > > > > > >> I want to discuss with you a few concerns I have >> about >> > our >> > > > > > current >> > > > > > > > > > >> vertex-centric model implementation, Spargel, now >> fully >> > > > > subsumed >> > > > > > > by >> > > > > > > > > > Gelly. >> > > > > > > > > > >> >> > > > > > > > > > >> Spargel is our implementation of Pregel [1], but it >> > > violates >> > > > > > some >> > > > > > > > > > >> fundamental properties of the model, as described in >> the >> > > > paper >> > > > > > and >> > > > > > > > as >> > > > > > > > > > >> implemented in e.g. Giraph, GPS, Hama. I often find >> > myself >> > > > > > > confused >> > > > > > > > > both >> > > > > > > > > > >> when trying to explain it to current Giraph users and >> > when >> > > > > > porting >> > > > > > > > my >> > > > > > > > > > >> Giraph algorithms to it. >> > > > > > > > > > >> >> > > > > > > > > > >> More specifically: >> > > > > > > > > > >> - in the Pregel model, messages produced in >> superstep n, >> > > are >> > > > > > > > received >> > > > > > > > > in >> > > > > > > > > > >> superstep n+1. In Spargel, they are produced and >> > consumed >> > > in >> > > > > the >> > > > > > > > same >> > > > > > > > > > >> iteration. >> > > > > > > > > > >> - in Pregel, vertices are active during a superstep, >> if >> > > they >> > > > > > have >> > > > > > > > > > received >> > > > > > > > > > >> a message in the previous superstep. In Spargel, a >> > vertex >> > > is >> > > > > > > active >> > > > > > > > > > during >> > > > > > > > > > >> a superstep if it has changed its value. >> > > > > > > > > > >> >> > > > > > > > > > >> These two differences require a lot of rethinking >> when >> > > > porting >> > > > > > > > > > >> applications >> > > > > > > > > > >> and can easily cause bugs. >> > > > > > > > > > >> >> > > > > > > > > > >> The most important problem however is that we require >> > the >> > > > user >> > > > > > to >> > > > > > > > > split >> > > > > > > > > > >> the >> > > > > > > > > > >> computation in 2 phases (2 UDFs): >> > > > > > > > > > >> - messaging: has access to the vertex state and can >> > > produce >> > > > > > > messages >> > > > > > > > > > >> - update: has access to incoming messages and can >> update >> > > the >> > > > > > > vertex >> > > > > > > > > > value >> > > > > > > > > > >> >> > > > > > > > > > >> Pregel/Giraph only expose one UDF to the user: >> > > > > > > > > > >> - compute: has access to both the vertex state and >> the >> > > > > incoming >> > > > > > > > > > messages, >> > > > > > > > > > >> can produce messages and update the vertex value. >> > > > > > > > > > >> >> > > > > > > > > > >> This might not seem like a big deal, but except from >> > > forcing >> > > > > the >> > > > > > > > user >> > > > > > > > > to >> > > > > > > > > > >> split their program logic into 2 phases, Spargel also >> > > makes >> > > > > some >> > > > > > > > > common >> > > > > > > > > > >> computation patterns non-intuitive or impossible to >> > > write. A >> > > > > > very >> > > > > > > > > simple >> > > > > > > > > > >> example is propagating a message based on its value >> or >> > > > sender >> > > > > > ID. >> > > > > > > To >> > > > > > > > > do >> > > > > > > > > > >> this with Spargel, one has to store all the incoming >> > > > messages >> > > > > in >> > > > > > > the >> > > > > > > > > > >> vertex >> > > > > > > > > > >> value (might be of different type btw) during the >> > > messaging >> > > > > > phase, >> > > > > > > > so >> > > > > > > > > > that >> > > > > > > > > > >> they can be accessed during the update phase. >> > > > > > > > > > >> >> > > > > > > > > > >> So, my first question is, when implementing Spargel, >> > were >> > > > > other >> > > > > > > > > > >> alternatives considered and maybe rejected in favor >> of >> > > > > > performance >> > > > > > > > or >> > > > > > > > > > >> because of some other reason? If someone knows, I >> would >> > > love >> > > > > to >> > > > > > > hear >> > > > > > > > > > about >> > > > > > > > > > >> them! >> > > > > > > > > > >> >> > > > > > > > > > >> Second, I wrote a prototype implementation [2] that >> only >> > > > > exposes >> > > > > > > one >> > > > > > > > > > UDF, >> > > > > > > > > > >> compute(), by keeping the vertex state in the >> solution >> > set >> > > > and >> > > > > > the >> > > > > > > > > > >> messages >> > > > > > > > > > >> in the workset. This way all previously mentioned >> > > > limitations >> > > > > go >> > > > > > > > away >> > > > > > > > > > and >> > > > > > > > > > >> the API (see "SSSPComputeFunction" in the example >> [3]) >> > > > looks a >> > > > > > lot >> > > > > > > > > more >> > > > > > > > > > >> like Giraph (see [4]). >> > > > > > > > > > >> >> > > > > > > > > > >> I have not run any experiments yet and the prototype >> has >> > > > some >> > > > > > ugly >> > > > > > > > > > hacks, >> > > > > > > > > > >> but if you think any of this makes sense, then I'd be >> > > > willing >> > > > > to >> > > > > > > > > follow >> > > > > > > > > > up >> > > > > > > > > > >> and try to optimize it. If we see that it performs >> well, >> > > we >> > > > > can >> > > > > > > > > consider >> > > > > > > > > > >> either replacing Spargel or adding it as an >> alternative. >> > > > > > > > > > >> >> > > > > > > > > > >> Thanks for reading this long e-mail and looking >> forward >> > to >> > > > > your >> > > > > > > > input! >> > > > > > > > > > >> >> > > > > > > > > > >> Cheers, >> > > > > > > > > > >> -Vasia. >> > > > > > > > > > >> >> > > > > > > > > > >> [1]: >> https://kowshik.github.io/JPregel/pregel_paper.pdf >> > > > > > > > > > >> [2]: >> > > > > > > > > > >> >> > > > > > > > > > >> >> > > > > > > > > > >> > > > > > > > > >> > > > > > > > >> > > > > > > >> > > > > > >> > > > > >> > > > >> > > >> > >> https://github.com/vasia/flink/tree/spargel-2/flink-libraries/flink-gelly/src/main/java/org/apache/flink/graph/spargelnew >> > > > > > > > > > >> [3]: >> > > > > > > > > > >> >> > > > > > > > > > >> >> > > > > > > > > > >> > > > > > > > > >> > > > > > > > >> > > > > > > >> > > > > > >> > > > > >> > > > >> > > >> > >> https://github.com/vasia/flink/blob/spargel-2/flink-libraries/flink-gelly/src/main/java/org/apache/flink/graph/spargelnew/example/SSSPCompute.java >> > > > > > > > > > >> [4]: >> > > > > > > > > > >> >> > > > > > > > > > >> >> > > > > > > > > > >> > > > > > > > > >> > > > > > > > >> > > > > > > >> > > > > > >> > > > > >> > > > >> > > >> > >> https://github.com/grafos-ml/okapi/blob/master/src/main/java/ml/grafos/okapi/graphs/SingleSourceShortestPaths.java >> > > > > > > > > > >> >> > > > > > > > > > >> >> > > > > > > > > > >> > > > > > > > > >> > > > > > > > >> > > > > > > >> > > > > > >> > > > > >> > > > >> > > >> > >> > >