Use thrust to find element in groups - cuda

I have two int vectors for keys and values, their size is about 500K.
The key vector is already sorted. And there are 10K groups approximately.
The value is non-negative(stands for useful) or -2(stands for no use), in each group there should be one or zero non-negative values, and the rest is -2.
key: 0 0 0 0 1 2 2 3 3 3 3
value:-2 -2 1 -2 3 -2 -2 -2 -2 -2 0
The third pair of group 0 [0 1] is useful. For group 1 we get the pair[1 3]. The values of group 2 are all -2, so we get nothing. And for group 3, the result is [3 0].
So, the question is how can I do this by thrust or cuda ?
Here are two ideas.
First one:
Get the number of each group by a histogram algorithm. So the barrier of each group can be computed.
Operate thrust::find_if on each group to get the useful element.
Second one:
Use thrust::transform to add 2 for every value and now all the value are non-negative, and zero stands for useless.
Use thrust::reduce_by_key to get the reduction for every group, and then subtract 2 for every output value.
I think there must be some other methods which will achieve much more performance than the above two.
Performance of the methods:
I have test the Second method above and the method given by #Robert Crovella, ie. reduce_by_key and remove_if method.
The size of the vectors is 2691028, the vectors consist of 100001 groups. Here is their average time:
reduce_by_key: 1204ms
remove_if: 192ms
From above result, we can see that remove_if method is much faster. And also the "remove_if" method is easy to implement and consume much less gpu memory.
Briefly, #Robert Crovella 's method is very good.

I would use a thrust::zip_iterator to zip the key and values pairs together, and then I would do a thrust::remove_if operation on the zipped values which would require a functor definition that would indicate to remove every pair for which the value is negative (or whatever test you wish.)
Here's a worked example:
$ cat t1009.cu
#include <thrust/remove.h>
#include <thrust/device_vector.h>
#include <thrust/iterator/zip_iterator.h>
#include <thrust/copy.h>
struct remove_func
{
template <typename T>
__host__ __device__
bool operator()(T &t){
return (thrust::get<1>(t) < 0); // could change to other kinds of tests
}
};
int main(){
int keys[] = {0,0,0,0,1,2,2,3,3,3,3};
int vals[] = {-2,-2,1,-2,3,-2,-2,-2,-2,-2,0};
size_t dsize = sizeof(keys)/sizeof(int);
thrust::device_vector<int>dkeys(keys, keys+dsize);
thrust::device_vector<int>dvals(vals, vals+dsize);
auto zr = thrust::make_zip_iterator(thrust::make_tuple(dkeys.begin(), dvals.begin()));
size_t rsize = thrust::remove_if(zr, zr+dsize, remove_func()) - zr;
thrust::copy_n(dkeys.begin(), rsize, std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl;
thrust::copy_n(dvals.begin(), rsize, std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl;
return 0;
}
$ nvcc -std=c++11 -o t1009 t1009.cu
$ ./t1009
0,1,3,
1,3,0,
$

Related

How to order a vector using some predefined sequence in Thrust

I have a predefined sequence of elements like this in a vector, the vector contains thousands of elements :
207.1 226.1 229.1 231.1 210.1 239.1 235.1 201.1 247.1 245.1 197.1 203.1 246.1 249.1 196.1 248.1 244.1 238.1
In a different vector I have the same elements as in the predefined vector but in a scattered way like this
226.1 225.1 205.1 220.1 220.1 237.1 226.1 212.1 212.1 205.1 205.1 202.1 202.1 192.1 192.1 191.1 191.1 192.1 192.1 192.1
Now I want to club up the elements in the scattered vector so that the order of the predefined vector is maintained, so the result should be like this
207.1 207.1 207.1 226.1 226.1 226.1 226.1 229.1 229.1 229.1 229.1 . . .
Is there any way to do this using CUDA thrust?
I'll make a few assumptions that I think are necessary:
Your first "predefined" sequence has no duplicates. If there were duplicates, and they were not adjacent, I cannot come up with an ordering strategy
Your second "scattered" sequence does not have any elements which are not also in the first sequence. If there were, I would have no idea where to place these or how to order them
With those assumptions, here is one possible method, using the above definitions of "first" and "second" sequences:
For the first sequence, provide a vector of the same length (the "index" sequence) that indicates the index of the value:
207.1 226.1 229.1 231.1 ...
0 1 2 3 ...
Perform a sort_by_key to order the first sequence. The index sequence will now be scrambled.
Using the ordered first sequence, use thrust::lower_bound on the second sequence, to find which value of the first sequence it matches.
Using the matching value's index for each element in the second sequence (via thrust::permutation_iterator), sort_by_key the second sequence by matching value index.
Here is an example:
$ cat t41.cu
#include <thrust/sort.h>
#include <thrust/binary_search.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>
#include <thrust/sequence.h>
#include <thrust/copy.h>
#include <thrust/iterator/permutation_iterator.h>
#include <iostream>
#include <cstdlib>
const int max_samples_per_element = 4;
typedef float dt;
typedef int it;
int main(){
// seq 1 and 2 data setup
// host
dt seq_1[] = {207.1, 226.1, 229.1, 231.1, 210.1, 239.1, 235.1, 201.1, 247.1, 245.1, 197.1, 203.1, 246.1, 249.1, 196.1, 248.1, 244.1, 238.1};
it seq_1_sz = sizeof(seq_1)/sizeof(seq_1[0]);
thrust::host_vector<dt> seq_2_hv;
for (int i = seq_1_sz-1; i >= 0; i--){
int element_samples = rand()%max_samples_per_element;
element_samples++;
for (int j = 0; j < element_samples; j++)
seq_2_hv.push_back(seq_1[i]);
}
// device
thrust::device_vector<dt> seq_2_dv = seq_2_hv;
thrust::device_vector<dt> seq_1_dv(seq_1, seq_1+seq_1_sz);
thrust::device_vector<it> index(seq_1_dv.size());
thrust::device_vector<it> index2(seq_2_dv.size());
thrust::sequence(index.begin(), index.end());
//process data
thrust::sort_by_key(seq_1_dv.begin(), seq_1_dv.end(), index.begin());
thrust::lower_bound(seq_1_dv.begin(), seq_1_dv.end(), seq_2_dv.begin(), seq_2_dv.end(), index2.begin());
auto my_pi = thrust::make_permutation_iterator(index.begin(), index2.begin());
thrust::sort_by_key(my_pi, my_pi+index2.size(), seq_2_dv.begin());
// display results
thrust::host_vector<dt> result = seq_2_dv;
thrust::copy_n(seq_1, seq_1_sz, std::ostream_iterator<dt>(std::cout, ","));
std::cout << std::endl;
thrust::copy(result.begin(), result.end(), std::ostream_iterator<dt>(std::cout, ","));
std::cout << std::endl;
}
$ nvcc -arch=sm_35 -o t41 t41.cu -O3 -lineinfo -Wno-deprecated-gpu-targets -std=c++14
$ ./t41
207.1,226.1,229.1,231.1,210.1,239.1,235.1,201.1,247.1,245.1,197.1,203.1,246.1,249.1,196.1,248.1,244.1,238.1,
207.1,207.1,207.1,226.1,229.1,229.1,229.1,231.1,231.1,231.1,231.1,210.1,210.1,210.1,210.1,239.1,239.1,239.1,235.1,235.1,235.1,235.1,201.1,201.1,201.1,247.1,247.1,245.1,245.1,197.1,203.1,203.1,203.1,246.1,246.1,246.1,246.1,249.1,249.1,196.1,196.1,196.1,196.1,248.1,248.1,244.1,244.1,244.1,238.1,238.1,238.1,238.1,
$
I'm not suggesting the above code is defect-free or suitable for any particular purpose. My objective here is to demonstrate a possible method, not provide a fully tested code.

How to dynamically set the size of device_vectors in thrust set operations?

I have two sets A & B. The result(C) of my operation should have elements in A which are not there in B. I use set_difference to do it. However the size of result(C) has to be set before the operation. Else it has extra zeros at the end, like below:
A=
1 2 3 4 5 6 7 8 9 10
B=
1 2 8 11 7 4
C=
3 5 6 9 10 0 0 0 0 0
How to set the size of result(C) dynamically so that output is C= 3 5 6 9. In a real problem, I would not know the required size of result device_vector apriori.
My code:
#include <thrust/execution_policy.h>
#include <thrust/set_operations.h>
#include <thrust/sequence.h>
#include <thrust/execution_policy.h>
#include <thrust/device_vector.h>
void remove_common_elements(thrust::device_vector<int> A, thrust::device_vector<int> B, thrust::device_vector<int>& C)
{
thrust::sort(thrust::device, A.begin(), A.end());
thrust::sort(thrust::device, B.begin(), B.end());
thrust::set_difference(thrust::device, A.begin(), A.end(), B.begin(), B.end(), C.begin());
}
int main(int argc, char * argv[])
{
thrust::device_vector<int> A(10);
thrust::sequence(thrust::device, A.begin(), A.end(),1); // x components of the 'A' vectors
thrust::device_vector<int> B(6);
B[0]=1;B[1]=2;B[2]=8;B[3]=11;B[4]=7;B[5]=4;
thrust::device_vector<int> C(A.size());
std::cout << "A="<< std::endl;
thrust::copy(A.begin(), A.end(), std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
std::cout << "B="<< std::endl;
thrust::copy(B.begin(), B.end(), std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
remove_common_elements(A, B, C);
std::cout << "C="<< std::endl;
thrust::copy(C.begin(), C.end(), std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
return 0;
}
In the general case (i.e. across various thrust algorithms) there is often no way to know the output size, except what the upper bound would be. The usual approach here would be to pass a result vector whose size is the upper bound of the possible output size. As you stated already, in many cases the actual size of the output cannot be known a-priori. Thrust has no particular magic to solve this. After the operation, you will know the size of the result, and it could be copied to a new vector if the "extra zeroes" were a problem for some reason (I can't think of a reason why they would be a problem generally, except that they use up allocated space).
If this is highly objectionable, one possibility (copying this information from a response by Jared Hoberock in another forum) is to run the algorithm twice, the first time using a discard_iterator (for the output data) and the second time with a real iterator, pointing to an actual vector allocation, of the requisite size. During the first pass, the discard_iterator is used to count the size of the actual result data, even though it is not stored anywhere. Quoting directly from Jared:
In the first phase, pass a discard_iterator as the output iterator. You can compare the discard_iterator returned as the result to compute the size of the output. In the second phase, call the algorithm "for real" and output into an array sized using the result of the first phase.
The technique is demonstrated in the set_operations.cu example [0,1]:
[0] https://github.com/thrust/thrust/blob/master/examples/set_operations.cu#L25
[1] https://github.com/thrust/thrust/blob/master/examples/set_operations.cu#L127
thrust::set_difference returns an iterator to the end of the resulting range.
If you just want to change the logical size of C to the number of resulting elements, you could simply erase the range "behind" the result range.
void remove_common_elements(thrust::device_vector<int> A,
thrust::device_vector<int> B, thrust::device_vector<int>& C)
{
thrust::sort(thrust::device, A.begin(), A.end());
thrust::sort(thrust::device, B.begin(), B.end());
auto C_end = thrust::set_difference(thrust::device, A.begin(), A.end(), B.begin(), B.end(), C.begin());
C.erase(C_end, C.end());
}

CUDA: method to calculate all partial sums during a sum reduction

I run into this issue over and over in CUDA. I have, for a set of elements, done some GPU calculation. This results in some value that has linear meaning (for instance, in terms of memory):
element_sizes = [ 10, 100, 23, 45 ]
And now, for the next stage of GPU calculation, I need the following values:
memory_size = sum(element_sizes)
memory_offsets = [ 0, 10, 110, 133 ]
I can calculate memory_size at 80 gbps on my GPU using the reduction code available from NVIDIA. However, I can't use this code, as it uses a branching technique that does not compose the memory offsets array. I have tried many things, but what I have found is that simply copying over elements_sizes to the host and calculating the offsets with a simd for loop is the simplest, fastest, way to go:
// in pseudo code
host_element_sizes = copy_to_host(element_sizes);
host_offsets = (... *) malloc(...);
int total_size = 0;
for(int i = 0; i < ...; ...){
host_offsets[i] = total_size;
total_size += host_element_sizes[i];
}
device_offsets = (... *) device_malloc(...);
device_offsets = copy_to_device(host_offsets,...);
However, I have done this many times now, and it is starting to become a bottleneck. This seems like a typical problem, but I have found no work-around.
What is the expected way for a CUDA programmer to solve this problem?
I think the algorithm you are looking for is a prefix sum. A prefix sum on a vector produces another vector which contains the cumulative sum values of the input vector. A prefix sum exists in at least two variants - an exclusive scan or an inclusive scan. Conceptually these are similar.
If your element_sizes vector has been deposited in GPU global memory (it appears to be the case based on your pseudocode), then there exist library functions that run on the GPU that you could call at that point, to produce the memory_offsets data (vector), and the memory_size value could be trivially obtained from the last value in the vector, with a slight variation based on whether you are doing an inclusive scan or exclusive scan.
Here's a trivial worked example using thrust:
$ cat t319.cu
#include <thrust/scan.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>
#include <thrust/copy.h>
#include <iostream>
int main(){
const int element_sizes[] = { 10, 100, 23, 45 };
const int ds = sizeof(element_sizes)/sizeof(element_sizes[0]);
thrust::device_vector<int> dv_es(element_sizes, element_sizes+ds);
thrust::device_vector<int> dv_mo(ds);
thrust::exclusive_scan(dv_es.begin(), dv_es.end(), dv_mo.begin());
std::cout << "element_sizes:" << std::endl;
thrust::copy_n(dv_es.begin(), ds, std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl << "memory_offsets:" << std::endl;
thrust::copy_n(dv_mo.begin(), ds, std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl << "memory_size:" << std::endl << dv_es[ds-1] + dv_mo[ds-1] << std::endl;
}
$ nvcc -o t319 t319.cu
$ ./t319
element_sizes:
10,100,23,45,
memory_offsets:
0,10,110,133,
memory_size:
178
$

Using Thrust to construct multiple histograms from a single array

Let us say that I have a single array which stores time stamps for multiple events. For example, T1_e1, T2_e1,....,T1_e2, T2_e2, T3_e2,.....T1_eN, T2,eN,..
I know that Thrust offers a function which computes adjacent differences, but here I need to do it for multiple events. Basically, constructing multiple histograms from a single input array.
So the output would have N different histograms (one for each event) like this:
histogram bins for e1, histogram bins for e2, histogram bins for e3,....histogram bins for eN.
Input1 (timestamps): 100, 101, 104, 105, 101,104, 106, 111, 90, 91, 93, 94,95
Input2 (events): 4123,4123,4123,4123,2129,2129,2129,2129,300,300,300,300,300
output: 4123:(1,2),(2,0),(3,1),(4,0),(5,0)
2129:(1,0),(2,1),(3,1),(4,0),(5,1)
300: (1,2),(2,1),(3,0),(4,),(5,0)
The number of bins will be fixed, i.e. 5 bins per histogram.
Regarding the tuples: (x,y) -> x is the difference between two consecutive time stamps belonging to the same event. y is the count.
If we consider event 4123, the first tuple is (1,2), because the difference between 101 and 100 is 1, and 105 and 104 is 1. So there are two time stamp differences which belong to this bin, hence (1,2).
Can someone please suggest the most efficient way to do this. So far, it seems that I will have to write my own code. But if there are existing solutions, I would like to try them first.
Here's one possible approach that computes the sparse histogram (i.e. non-zero bins only). It should not be difficult to convert a sparse histogram to a dense histogram (zero and non-zero bins included) using thrust scattering.
compute the timestamp differences using thrust::adjacent_difference and a special functor (my_adj_diff) that computes adjacent differences only for like events.
use thrust::remove_if to remove the zero values (one per event) at the start of each event sequence (created by the functor in step 1).
combine events and timestamp differences into a single integer for histogramming using thrust::transform. This just multiplies the event by 100 in my example, and adds the timestamp difference (assumed to be a set of bins of less than 100 max bin).
use the sparse histogram method from the thrust histogram example code.
Here's a fully worked example. The data does not quite match your expected output (non-zero values only) because you have an error in your expected output.
$ cat t678.cu
#include <thrust/adjacent_difference.h>
#include <thrust/iterator/zip_iterator.h>
#include <thrust/iterator/constant_iterator.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>
#include <thrust/copy.h>
#include <thrust/remove.h>
#include <thrust/sort.h>
#include <thrust/inner_product.h>
#include <thrust/reduce.h>
#include <thrust/functional.h>
#include <iostream>
#define ZIP(X,Y) thrust::make_zip_iterator(thrust::make_tuple(X,Y))
#define SCALE 100
struct my_adj_diff
{
template <typename T>
__host__ __device__
T operator()(T &d2, T &d1) const
{
if (thrust::get<1>(d1) == thrust::get<1>(d2)) {
thrust::get<0>(d2) -= thrust::get<0>(d1);}
else {
thrust::get<0>(d2) = 0;}
return d2;
}
};
struct my_is_zero
{
template <typename T>
__host__ __device__
bool operator()(const T &d1) const
{
return (thrust::get<0>(d1) == 0);
}
};
struct my_combine
{
template <typename T>
__host__ __device__
T operator()(const T &d1, const T &d2) const
{
return (d1*SCALE)+d2;
}
};
// sparse histogram using reduce_by_key
// modified from: https://github.com/thrust/thrust/blob/master/examples/histogram.cu
template <typename Vector1,
typename Vector2,
typename Vector3>
void sparse_histogram(const Vector1& input,
Vector2& histogram_values,
Vector3& histogram_counts)
{
typedef typename Vector1::value_type ValueType; // input value type
typedef typename Vector3::value_type IndexType; // histogram index type
thrust::device_vector<ValueType> data(input);
// sort data to bring equal elements together
thrust::sort(data.begin(), data.end());
// number of histogram bins is equal to number of unique values (assumes data.size() > 0)
IndexType num_bins = thrust::inner_product(data.begin(), data.end() - 1,
data.begin() + 1,
IndexType(1),
thrust::plus<IndexType>(),
thrust::not_equal_to<ValueType>());
// resize histogram storage
histogram_values.resize(num_bins);
histogram_counts.resize(num_bins);
// compact find the end of each bin of values
thrust::reduce_by_key(data.begin(), data.end(),
thrust::constant_iterator<IndexType>(1),
histogram_values.begin(),
histogram_counts.begin());
}
int main(){
int tstamps[] = { 100, 101, 104, 105, 101,104, 106, 111, 90, 91, 93, 94,95 };
int mevents[] = {4123,4123,4123,4123,2129,2129,2129,2129,300,300,300,300,300};
int dsize = sizeof(tstamps)/sizeof(int);
thrust::host_vector<int> h_stamps(tstamps, tstamps+dsize);
thrust::host_vector<int> h_events(mevents, mevents+dsize);
thrust::device_vector<int> d_stamps = h_stamps;
thrust::device_vector<int> d_events = h_events;
thrust::device_vector<int> diffs(dsize);
// compute timestamp differences by event
thrust::adjacent_difference(ZIP(d_stamps.begin(), d_events.begin()), ZIP(d_stamps.end(), d_events.end()), ZIP(d_stamps.begin(), d_events.begin()), my_adj_diff());
d_stamps[0] = 0; // fix up first event for adjacent_difference
int sz1 = thrust::remove_if(ZIP(d_stamps.begin(), d_events.begin()), ZIP(d_stamps.end(), d_events.end()), my_is_zero()) - ZIP(d_stamps.begin(), d_events.begin());
d_stamps.resize(sz1);
d_events.resize(sz1);
// pack events and timestamps into a single vector - assumes max bin (time difference) is less than SCALE
thrust::device_vector<int> d_data(sz1);
thrust::transform(d_events.begin(), d_events.end(), d_stamps.begin(), d_data.begin(), my_combine());
// compute histogram
thrust::device_vector<int> histogram_values;
thrust::device_vector<int> histogram_counts;
sparse_histogram(d_data, histogram_values, histogram_counts);
thrust::copy(histogram_values.begin(), histogram_values.end(), std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl;
thrust::copy(histogram_counts.begin(), histogram_counts.end(), std::ostream_iterator<int>(std::cout, ","));
std::cout << std::endl;
}
$ nvcc t678.cu -o t678
$ ./t678
30001,30002,212902,212903,212905,412301,412303,
3,1,1,1,1,2,1,
$
Note that you will need to use CUDA 7.0 (not CUDA 7.0 RC or any earlier version of CUDA) or else download the latest thrust master branch from github, because older versions of thrust have an issue when attempting to use zip iterators with thrust::adjacent_difference.

storing return value of thrust reduce_by_key on device vectors

I have been trying to use thrust function reduce_by_key on device vectors. In the documentation they have given example on host_vectors instead of any device vector. The main problem I am getting is in storing the return value of the function. To be more specific here is my code:
thrust::device_vector<int> hashValueVector(10)
thrust::device_vector<int> hashKeysVector(10)
thrust::device_vector<int> d_pathId(10);
thrust::device_vector<int> d_freqError(10); //EDITED
thrust::pair<thrust::detail::normal_iterator<thrust::device_ptr<int> >,thrust::detail::normal_iterator<thrust::device_ptr<int> > > new_end; //THE PROBLEM
new_end = thrust::reduce_by_key(hashKeysVector.begin(),hashKeysVector.end(),hashValueVector.begin(),d_pathId.begin(),d_freqError.begin());
I tried declaring them as device_ptr first in the definition since for host_vectors too they have used pointers in the definition in the documentation. But I am getting compilation error when I try that then I read the error statement and converted the declaration to the above, this is compiling fine but I am not sure whether this is the right way to define or not.
I there any other standard/clean way of declaring that (the "new_end" variable)? Please comment if my question is not clear somewhere.
EDIT: I have edited the declaration of d_freqError. It was supposed to be int I wrote it as hashElem by mistake, sorry for that.
There is a problem in the setup of your reduce_by_key operation:
new_end = thrust::reduce_by_key(hashKeysVector.begin(),hashKeysVector.end(),hashValueVector.begin(),d_pathId.begin(),d_freqError.begin());
Notice that in the documentation it states:
OutputIterator2 is a model of Output Iterator and and InputIterator2's value_type is convertible to OutputIterator2's value_type.
The value type of your InputIterator2 (i.e. hashValueVector.begin()) is int . The value type of your OutputIterator2 is struct hashElem. Thrust is not going to know how to convert an int to a struct hashElem.
Regarding your question, it should not be difficult to capture the return entity from reduce_by_key. According to the documentation it is a thrust pair of two iterators, and these iterators should be consistent with (i.e. of the same vector type and value type) as your keys iterator type and your values iterator type, respectively.
Here's an updated sample based on what you posted, which compiles cleanly:
$ cat t353.cu
#include <iostream>
#include <thrust/device_vector.h>
#include <thrust/pair.h>
#include <thrust/reduce.h>
#include <thrust/sequence.h>
#include <thrust/fill.h>
#include <thrust/copy.h>
typedef thrust::device_vector<int>::iterator dIter;
int main(){
thrust::device_vector<int> hashValueVector(10);
thrust::device_vector<int> hashKeysVector(10);
thrust::device_vector<int> d_pathId(10);
thrust::device_vector<int> d_freqError(10);
thrust::sequence(hashValueVector.begin(), hashValueVector.end());
thrust::fill(hashKeysVector.begin(), hashKeysVector.begin()+5, 1);
thrust::fill(hashKeysVector.begin()+6, hashKeysVector.begin()+10, 2);
thrust::pair<dIter, dIter> new_end;
new_end = thrust::reduce_by_key(hashKeysVector.begin(),hashKeysVector.end(),hashValueVector.begin(),d_pathId.begin(),d_freqError.begin());
std::cout << "Number of results are: " << new_end.first - d_pathId.begin() << std::endl;
thrust::copy(d_pathId.begin(), new_end.first, std::ostream_iterator<int>(std::cout, "\n"));
thrust::copy(d_freqError.begin(), new_end.second, std::ostream_iterator<int>(std::cout, "\n"));
}
$ nvcc -arch=sm_20 -o t353 t353.cu
$ ./t353
Number of results are: 3
1
0
2
10
5
30
$