Uh oh!
There was an error while loading. Please reload this page.
[SPARK-21190][PYSPARK] Python Vectorized UDFs - #18659
Conversation
The following was used to test performance locally spark=SparkSession.builder.appName("vectorized_udfs").getOrCreate()
vectorize=Trueifvectorize:
fromnumpyimportlog, expelse:
frommathimportlog, expdefmy_func(p1, p2):
w=0.5returnexp(log(p1) +log(p2) -log(w))
df=spark.range(1<<24, numPartitions=16).toDF("id") \
.withColumn("p1", rand()).withColumn("p2", rand())
my_udf=udf(my_func, DoubleType(), vectorized=vectorize)
df.withColumn("p", my_udf(col("p1"), col("p2")))** Updated with using |
Some comments on the performance above
|
SparkQA
commented
Jul 17, 2017
Test build #79680 has finished for PR 18659 at commit
|
SparkQA
commented
Jul 17, 2017
Test build #79682 has finished for PR 18659 at commit
|
| val genericRowData = fields.map { field => | ||
| field.getAccessor.getObject(_index) | ||
| }.toArray[Any] |
There was a problem hiding this comment.
How about using SpecificInternalRow to improve performance? I think that it could eliminate some boxing/unboxing. The following is a snippet for this usage.
valfieldTypes = fields.map { field =>
fieldmatch {
caseNullableIntVector => IntegerTypecaseNullableFloat8Vector => DoubleType
...
}
}
valrow = newSpecificInternalRow(fieldTypes)
fields.zipWithIndex.map { case (field, i) =>
fieldmatch {
caseNullableIntVector =>
row.setInt(i, field.asInstanceOf[NullableIntVector].getAccessor.get(_index)) caseNullableFloat8Vector => LongTyperow.setDouble(i, field.asInstanceOf[NullableFloat8Vector].getAccessor.get(_index))
... }
}There was a problem hiding this comment.
Thanks @kiszk , I'll give that a shot and see if it helps!
There was a problem hiding this comment.
I have implemented arrow -> unsafe row conversions in:
icexelloss@8f38c15#diff-52cca47e7a940849b28d476ddf99d65eR575
This reuses the row object and doesn't do boxing. Hopefully it's useful to you as well?
There was a problem hiding this comment.
@BryanCutler
As @cloud-fan suggested here, it is good to create ColumnarBatch with ArrowColumnVector and get an iterator. It looks simpler implementation.
cc: @ueshin
The following is code piece.
new Iterator[InternalRow] {
private val _allocator = new RootAllocator(Long.MaxValue)
private var _reader: ArrowFileReader = _
private var _root: VectorSchemaRoot = _
private var _index = 0
private var _iterator = null
loadNextBatch()
override def hasNext: Boolean = _root != null && _index < _root.getRowCount && _iterator.hasNext
override def next(): InternalRow = {
_index += 1
if (_index >= _root.getRowCount) {
_index = 0
loadNextBatch()
if (!hasNext) {
close()
}
}
_iterator.next()
}
...
private def loadNextBatch(): Unit = {
closeReader()
if (iter.hasNext) {
val in = new ByteArrayReadableSeekableByteChannel(iter.next().asPythonSerializable)
_reader = new ArrowFileReader(in, _allocator)
_root = _reader.getVectorSchemaRoot // throws IOException
_reader.loadNextBatch() // throws IOException
val arrowSchema = ArrowUtils.fromArrowSchema(_root.getSchema)
val fields = _root.getFieldVectors
val rows = _root.getRowCount
val columnarBatch = ColumnarBatch.allocateArrow(
_root.getFieldVectors.asInstanceOf[java.util.List[ValueVector]],
ArrowUtils.fromArrowSchema(_root.getSchema), _root.getRowCount)
_iterator = columnarBatch.rowIterator
}
}
public final class ColumnarBatch {
...
public static ColumnarBatch allocateArrow(List<ValueVector> vectors, StructType schema, int maxRows) {
// need to implement the following constructor for arrowColumnVector
return new ColumnarBatch(vectors, schema, maxRows);
}
...
}
There was a problem hiding this comment.
@ueshin I made some changes here to allow for use with ArrowColumnVectors. I was thinking of putting these in a separate JIRA because it can be used regardless of what is done with vectorized UDFs. What do you think?
There was a problem hiding this comment.
@BryanCutler I agree with you, let's separate it from this pr.
There was a problem hiding this comment.
ok, will do. I created https://issues.apache.org/jira/browse/SPARK-21583 for this
SparkQA
commented
Jul 29, 2017
Test build #80030 has finished for PR 18659 at commit
|
46e4112 to
912143eCompareSparkQA
commented
Aug 4, 2017
Test build #80264 has finished for PR 18659 at commit
|
SparkQA
commented
Aug 5, 2017
Test build #80265 has finished for PR 18659 at commit
|
a01a2d3 to
38474d8CompareSparkQA
commented
Aug 25, 2017
Test build #81138 has finished for PR 18659 at commit
|
38474d8 to
cc7ed5aCompareSparkQA
commented
Sep 1, 2017
Test build #81321 has finished for PR 18659 at commit
|
SparkQA
commented
Sep 6, 2017
Test build #81478 has finished for PR 18659 at commit
|
1503fa0 to
fdea603Comparefdea603 to
4f6c950Compare| @since(1.3) | ||
| def udf(f=None, returnType=StringType()): | ||
| def udf(f=None, returnType=StringType(), vectorized=False): |
There was a problem hiding this comment.
@felixcheung does this fit your idea for a more generic decorator? Not exclusively labeled as pandas_udf, just enable vectorization with a flag, e.g. @udf(DoubleType(), vectorized=True)
There was a problem hiding this comment.
I think @pandas_udf(DoubleType()) is better than @udf(DoubleType(), vectorized=True), which is more concise.
There was a problem hiding this comment.
as we discussed in the email, we should also accept data type of string format.
There was a problem hiding this comment.
and also **kwargs to bring the size information
There was a problem hiding this comment.
It seems like the consensus is for pandas_udf and I'm fine with that too. I'll make that change and the others brought up here.
felixcheung
commented
Sep 6, 2017
via email
Cool! |
| val outputRowIterator = ArrowConverters.fromPayloadIterator( | ||
| outputIterator.map(new ArrowPayload(_)), context) | ||
| assert(schemaOut.equals(outputRowIterator.schema)) |
There was a problem hiding this comment.
@felixcheung , I think you had also brought up checking the return type matches what was defined in the UDF. This is done here.
SparkQA
commented
Sep 7, 2017
Test build #81479 has finished for PR 18659 at commit
|
| series = [series] | ||
| series = [(s, None) if not isinstance(s, (list, tuple)) else s for s in series] | ||
| arrs = [pa.Array.from_pandas(s[0], type=s[1], mask=s[0].isnull()) for s in series] | ||
| batch = pa.RecordBatch.from_arrays(arrs, ["_%d" % i for i in range(len(arrs))]) |
| if not isinstance(series, (list, tuple)) or \ | ||
| (len(series) == 2 and isinstance(series[1], pa.DataType)): | ||
| series = [series] | ||
| series = [(s, None) if not isinstance(s, (list, tuple)) else s for s in series] |
There was a problem hiding this comment.
I'd use generator comprehension.
There was a problem hiding this comment.
That would work, but does it help much since series will already be a list or tuple?
There was a problem hiding this comment.
Yea, it actually affects the performance because we can avoid an extra loop:
defim_map(x):
print("I am map %s"%x)
returnxdefim_gen(x):
print("I am gen %s"%x)
returnxdefim_list(x):
print("I am list %s"%x)
returnxitems=list(range(3))
map(im_map, [im_list(item) foriteminitems])
map(im_map, (im_gen(item) foriteminitems))And .. this actually affects the performance up to my knowledge:
importtimeitems=list(xrange(int(1e8)))
for_inxrange(10):
s=time.time()
_=map(lambdax: x, [itemforiteminitems])
print"I am list comprehension with a list: %s"% (time.time() -s)
s=time.time()
_=map(lambdax: x, (itemforiteminitems))
print"I am generator expression with a list: %s"% (time.time() -s)This gives me ~13% improvement in Python 2
There was a problem hiding this comment.
This might not be a big deal but .. I usually use generator if it iterates once and is discarded. This should consume less memory too as list comprehension should be evaluated once first up to my knowledge.
There was a problem hiding this comment.
Thanks @HyukjinKwon , I suppose if there are more than a few series then it might make some difference. In that case, every little bit helps so sounds good to me!
| reader = pa.RecordBatchFileReader(pa.BufferReader(obj)) | ||
| batches = [reader.get_batch(i) for i in range(reader.num_record_batches)] | ||
| # NOTE: a 0-parameter pandas_udf will produce an empty batch that can have num_rows set | ||
| num_rows = sum([batch.num_rows for batch in batches]) |
There was a problem hiding this comment.
I'd use generator comprehension here too.
There was a problem hiding this comment.
I guess this makes sense because its a summation, no sense in making a list then adding it all up
| """ | ||
| import pyarrow as pa | ||
| reader = pa.RecordBatchFileReader(pa.BufferReader(obj)) | ||
| batches = [reader.get_batch(i) for i in range(reader.num_record_batches)] |
cloud-fan
commented
Sep 19, 2017
what if users installed an older version of pyarrow? Shall we throw exception and ask them to upgrade, or work around type casting issue? |
SparkQA
commented
Sep 19, 2017
Test build #81945 has finished for PR 18659 at commit
|
BryanCutler
commented
Sep 19, 2017
Thanks for the reviews @ueshin@viirya and @HyukjinKwon ! I updated with your comments |
BryanCutler
commented
Sep 20, 2017
@cloud-fan , in regards to handling of problems that might come up if using different versions of Arrow, I think we should first decide on a minimum supported version, then maybe we could put that version of pyarrow as a requirement for PySpark. If we decide to use 0.4.1 which we currently use, then we should probably work around the type casting issue and make sure this PR works with that version. |
SparkQA
commented
Sep 20, 2017
Test build #81955 has finished for PR 18659 at commit
|
cloud-fan
commented
Sep 20, 2017
ok let's work around the type casting issue and discuss arrow upgrading later. |
| * \ / | ||
| * \ socket (input of UDF) | ||
| * \ / | ||
| * upstream (from child) |
There was a problem hiding this comment.
Maybe I put myself uncomfortable to see Downstream upper, forgive me..
There was a problem hiding this comment.
that's fine but either looks fine and not a big deal.
BryanCutler
commented
Sep 20, 2017
@ueshin I haven't had much luck with the casting workaround: It appears that it forces a copy for floating point -> integer and then checks if any NaNs, so I get the error |
@BryanCutler Hmm, I'm not exactly sure the reason why it doesn't work (or mine works) but I guess we can use |
BryanCutler
commented
Sep 21, 2017
Thanks @ueshin , that works to allow the tests to pass. I do worry that it might cause some other issues and I would much prefer we upgrade Arrow to handle this, but I'll push this and we can discuss. |
SparkQA
commented
Sep 21, 2017
Test build #82042 has finished for PR 18659 at commit
|
SparkQA
commented
Sep 22, 2017
Test build #82053 has finished for PR 18659 at commit
|
| """ | ||
| def __init__(self): | ||
| super(ArrowPandasSerializer, self).__init__() |
There was a problem hiding this comment.
No, that was leftovers.. I'll remove it in a followup.
cloud-fan
commented
Sep 22, 2017
LGTM, merging to master! We can address remaining minor comments in follow-up, and have new PRs to remove the 0-parameter UDF and use arrow streaming protocol. |
BryanCutler
commented
Sep 22, 2017
Thanks @cloud-fan@ueshin and others who reviewed! I'll make followups to disable 0-param and complete the docs for this. |
What changes were proposed in this pull request?
This PR adds vectorized UDFs to the Python API
Proposed API
Introduce a flag to turn on vectorization for a defined UDF, for example:
or
Usage is the same as normal UDFs
0-parameter UDFs
pandas_udf functions can declare an optional
**kwargsand when evaluated, will contain a key "size" that will give the required length of the output. For example:How was this patch tested?
Added new unit tests in pyspark.sql that are enabled if pyarrow and Pandas are available.
TODO