Skip to content

Commit 5f3833b

Browse files
thomcomtrxcllnt
andauthored
Fix target/source keymapping (#402)
* Insane errors are percolating. * Fix #397 * Forgotten files, ugh. * Typos in schema. * Update modules/demo/api-server/util/gpu_cache.js Co-authored-by: Paul Taylor <[email protected]> * Update modules/demo/api-server/util/gpu_cache.js Co-authored-by: Paul Taylor <[email protected]> Co-authored-by: Paul Taylor <[email protected]>
1 parent f3b42fe commit 5f3833b

File tree

7 files changed

+258
-31
lines changed

7 files changed

+258
-31
lines changed

modules/demo/api-server/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ const env = {
2929
};
3030

3131
spawnSync(process.execPath,
32-
[fastify, 'start', '-l', 'info', '-P', '-w', 'app.js'],
32+
[fastify, 'start', '-l', 'info', '-P', '-p', '3010', '-w', 'app.js'],
3333
{env, cwd: __dirname, stdio: 'inherit'});

modules/demo/api-server/routes/graphology/index.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,16 @@ module.exports = async function(fastify, opts) {
304304
let tiled = Series.sequence({type: new Float32, init: 0.0, size: (6 * edges.numRows)});
305305
let base_offset = Series.sequence({type: new Int32, init: 0.0, size: edges.numRows}).mul(3);
306306
//
307-
// Duplicatin the sigma.j createNormalizationFunction here because there's no other way
308-
// to let the Graph object compute it.
307+
// Duplicatin the sigma.js createNormalizationFunction here because this is the best way
308+
// to let the Graph object compute it on GPU.
309309
//
310-
let source = edges.get('source');
311-
let target = edges.get('target');
310+
// Remap the indices in the key table to their real targets. See
311+
// https://github.com/rapidsai/node/issue/397
312+
let keys = df.get('key');
313+
let source_map = edges.get('source');
314+
let source = keys.gather(source_map, false);
315+
let target_map = edges.get('target');
316+
let target = keys.gather(target_map, false);
312317
let x = df.get('x');
313318
let y = df.get('y');
314319
const [xMin, xMax] = x.minmax();
@@ -318,10 +323,10 @@ module.exports = async function(fastify, opts) {
318323
const dY = (yMax + yMin) / 2.0;
319324
x = x.add(-1.0 * dX).mul(1.0 / ratio).add(0.5);
320325
y = y.add(-1.0 * dY).mul(1.0 / ratio).add(0.5);
321-
const source_xmap = x.gather(source.cast(new Int32));
322-
const source_ymap = y.gather(source.cast(new Int32));
323-
const target_xmap = x.gather(target.cast(new Int32));
324-
const target_ymap = y.gather(target.cast(new Int32));
326+
const source_xmap = x.gather(source, false);
327+
const source_ymap = y.gather(source, false);
328+
const target_xmap = x.gather(target, false);
329+
const target_ymap = y.gather(target, false);
325330
const color = Series.new(['#999'])
326331
.hexToIntegers(new Int32)
327332
.bitwiseOr(0xff000000)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) 2022, NVIDIA CORPORATION.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const json_good = {
16+
'json_good.txt':
17+
` {
18+
"nodes":
19+
[
20+
{
21+
"key": "customer data management",
22+
"label": "Customer data management",
23+
"tag": "Field",
24+
"URL": "https://en.wikipedia.org/wiki/Customer%20data%20management",
25+
"cluster": "7",
26+
"x": -278.2200012207031,
27+
"y": 436.0100402832031,
28+
"score": 0
29+
},
30+
{
31+
"key": "educational data mining",
32+
"label": "Educational data mining",
33+
"tag": "Field",
34+
"URL": "https://en.wikipedia.org/wiki/Educational%20data%20mining",
35+
"cluster": "7",
36+
"x": -1.9823756217956543,
37+
"y": 250.4990692138672,
38+
"score": 0
39+
}
40+
],
41+
"edges":
42+
[
43+
["office suite", "human interactome"],
44+
["educational data mining", "human interactome"],
45+
],
46+
"clusters":
47+
[
48+
{"key": "0", "color": "#6c3e81", "clusterLabel": "human interactome"},
49+
{"key": "1", "color": "#666666", "clusterLabel": "Spreadsheets"},
50+
],
51+
"tags": [
52+
{"key": "Chart type", "image": "charttype.svg"},
53+
{"key": "Company", "image": "company.svg"},
54+
]
55+
} `
56+
};
57+
58+
const json_large = {
59+
'json_large.txt':
60+
` {
61+
"attributes": {},
62+
"nodes": [
63+
{
64+
"key": "0",
65+
"attributes": {
66+
"cluster": 0,
67+
"x": -13.364310772761677,
68+
"y": 4.134339113107921,
69+
"size": 0,
70+
"label": "Node n°291, in cluster n°0",
71+
"color": "#e24b04"
72+
}
73+
},
74+
{
75+
"key": "1",
76+
"attributes": {
77+
"cluster": 1,
78+
"x": 1.3836898237261988,
79+
"y": -11.536596764896206,
80+
"size": 1,
81+
"label": "Node n°292, in cluster n°1",
82+
"color": "#323455"
83+
}
84+
}
85+
],
86+
"edges": [
87+
{"key": "geid_115_98", "source": "291", "target": "290"},
88+
{"key": "geid_115_99", "source": "290", "target": "291"}
89+
],
90+
"options": {"type": "mixed", "multi": false, "allowSelfLoops": true}
91+
}`
92+
};
93+
94+
const json_out_of_order = {
95+
'json_out_of_order.txt':
96+
` {
97+
"attributes": {},
98+
"nodes": [
99+
{
100+
"key": "290",
101+
"attributes": {
102+
"cluster": 0,
103+
"x": -13.364310772761677,
104+
"y": 4.134339113107921,
105+
"size": 0,
106+
"label": "Node n°291, in cluster n°0",
107+
"color": "#e24b04"
108+
}
109+
},
110+
{
111+
"key": "291",
112+
"attributes": {
113+
"cluster": 1,
114+
"x": 1.3836898237261988,
115+
"y": -11.536596764896206,
116+
"size": 1,
117+
"label": "Node n°292, in cluster n°1",
118+
"color": "#323455"
119+
}
120+
}
121+
],
122+
"edges": [
123+
{"key": "geid_115_98", "source": "290", "target": "291"},
124+
{"key": "geid_115_99", "source": "291", "target": "290"}
125+
],
126+
"options": {"type": "mixed", "multi": false, "allowSelfLoops": true}
127+
}`
128+
};
129+
130+
module.exports = {
131+
json_good: json_good,
132+
json_large: json_large,
133+
json_out_of_order: json_out_of_order
134+
};

modules/demo/api-server/test/routes/graphology.test.js

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
'use strict'
1616

17-
const {dir} = require('console');
18-
const {test} = require('tap');
19-
const {build} = require('../helper');
20-
const {tableFromIPC, RecordBatchStreamWriter} = require('apache-arrow');
21-
const {json_large, json_good} = require('../fixtures.js');
17+
const {dir} = require('console');
18+
const {test} = require('tap');
19+
const {build} = require('../helper');
20+
const {tableFromIPC, RecordBatchStreamWriter} = require('apache-arrow');
21+
const {json_large, json_good, json_out_of_order} = require('../fixtures.js');
2222

2323
test('graphology root returns api description', async t => {
2424
const app = await build(t);
@@ -30,9 +30,9 @@ test('graphology root returns api description', async t => {
3030
read_json: {
3131
filename: 'A URI to a graphology json dataset file.',
3232
result: `Causes the node-rapids backend to attempt to load the json object specified
33-
by :filename. The GPU with attempt to parse the json file asynchronously and will
33+
by :filename. The GPU will attempt to parse the json file asynchronously and will
3434
return OK/ Not Found/ or Fail based on the file status.
35-
If the load is successful, three tables will be created in the node-rapids backend:
35+
If the load is successful, four tables will be created in the node-rapids backend:
3636
nodes, edges, clusters, and tags. The root objects in the json target must match
3737
these names and order.`,
3838
returns: 'Result OK/Not Found/Fail'
@@ -187,8 +187,7 @@ test('get_column', async (t) => {
187187
test('nodes', async (t) => {
188188
const dir = t.testdir(json_large);
189189
const rpath = '../../test/routes/' + dir.substring(dir.lastIndexOf('/')) + '/json_large.txt';
190-
console.log(rpath);
191-
const app = await build(t);
190+
const app = await build(t);
192191
const load =
193192
await app.inject({method: 'POST', url: '/graphology/read_large_demo?filename=' + rpath});
194193
const res = await app.inject(
@@ -235,18 +234,48 @@ test('edges', async (t) => {
235234
await app.inject({method: 'POST', url: '/graphology/read_large_demo?filename=' + rpath});
236235
const res = await app.inject(
237236
{method: 'GET', url: '/graphology/edges', header: {'accepts': 'application/octet-stream'}});
237+
t.equal(res.statusCode, 200);
238238
const table = tableFromIPC(res.rawPayload);
239239
const release = await app.inject({method: 'POST', url: '/graphology/release'});
240240
t.ok(table.getChild('edges'));
241241
t.same(table.getChild('edges').toArray(), new Float32Array([
242242
0.02944733388721943,
243243
1,
244244
-1.701910173408654e+38,
245-
0.9705526828765869,
246-
0,
245+
0.02944733388721943,
246+
1,
247247
-1.701910173408654e+38,
248-
0.9705526828765869,
249-
0,
248+
0.02944733388721943,
249+
1,
250+
-1.701910173408654e+38,
251+
0.02944733388721943,
252+
1,
253+
-1.701910173408654e+38
254+
]))
255+
});
256+
257+
test('edges out of order', async (t) => {
258+
const dir = t.testdir(json_out_of_order);
259+
const rpath =
260+
'../../test/routes/' + dir.substring(dir.lastIndexOf('/')) + '/json_out_of_order.txt';
261+
const app = await build(t);
262+
const load =
263+
await app.inject({method: 'POST', url: '/graphology/read_large_demo?filename=' + rpath});
264+
const res = await app.inject(
265+
{method: 'GET', url: '/graphology/edges', header: {'accepts': 'application/octet-stream'}});
266+
t.equal(res.statusCode, 200);
267+
const table = tableFromIPC(res.rawPayload);
268+
const release = await app.inject({method: 'POST', url: '/graphology/release'});
269+
t.ok(table.getChild('edges'));
270+
t.same(table.getChild('edges').toArray(), new Float32Array([
271+
0.02944733388721943,
272+
1,
273+
-1.701910173408654e+38,
274+
0.02944733388721943,
275+
1,
276+
-1.701910173408654e+38,
277+
0.02944733388721943,
278+
1,
250279
-1.701910173408654e+38,
251280
0.02944733388721943,
252281
1,

modules/demo/api-server/test/routes/root.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ test('root returns API description', async (t) => {
2727
read_json: {
2828
filename: 'A URI to a graphology json dataset file.',
2929
result: `Causes the node-rapids backend to attempt to load the json object specified
30-
by :filename. The GPU with attempt to parse the json file asynchronously and will
30+
by :filename. The GPU will attempt to parse the json file asynchronously and will
3131
return OK/ Not Found/ or Fail based on the file status.
32-
If the load is successful, three tables will be created in the node-rapids backend:
32+
If the load is successful, four tables will be created in the node-rapids backend:
3333
nodes, edges, clusters, and tags. The root objects in the json target must match
3434
these names and order.`,
3535
returns: 'Result OK/Not Found/Fail'

modules/demo/api-server/util/gpu_cache.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ function json_key_attributes_to_dataframe(str) {
2828
const dtypes = [new Int32, new Float32, new Float32, new Int32, new Utf8String, new Utf8String];
2929
const no_open_list = str.split('[\n').gather([1], false);
3030
const tokenized = no_open_list.split('},');
31+
const keys = tokenized.getJSONObject('.key');
32+
keys.setNullMask(1, 0);
33+
arr['key'] = keys.cast(new Int32);
3134
columns.forEach((col, ix) => {
3235
const parse_result = tokenized.getJSONObject('.attributes.' + columns[ix]);
33-
const string_array = Series.new(parse_result);
34-
arr[col] = string_array.cast(dtypes[ix]);
36+
parse_result.setNullMask([], 0);
37+
arr[col] = parse_result.cast(dtypes[ix]);
3538
});
3639
const result = new DataFrame(arr);
3740
return result;
@@ -43,7 +46,8 @@ function json_aos_to_dataframe(str, columns, dtypes) {
4346
const no_open_list = str.split('[\n').gather([1], false);
4447
const tokenized = no_open_list.split('},');
4548
const parse_result = tokenized.getJSONObject('.' + columns[ix]);
46-
arr[col] = Series.new(parse_result).cast(dtypes[ix]);
49+
parse_result.setNullMask(1, 0);
50+
arr[col] = parse_result.cast(dtypes[ix]);
4751
});
4852
const result = new DataFrame(arr);
4953
return result;
@@ -56,8 +60,8 @@ function json_aoa_to_dataframe(str, dtypes) {
5660
dtypes.forEach((_, ix) => {
5761
const get_ix = `[${ix}]`;
5862
const parse_result = tokenized.getJSONObject(get_ix);
59-
const string_array = Series.new(parse_result);
60-
arr[ix] = string_array.cast(dtypes[ix]);
63+
parse_result.setNullMask([], 0);
64+
arr[ix] = parse_result.cast(dtypes[ix]);
6165
});
6266
const result = new DataFrame(arr);
6367
return result;
@@ -99,7 +103,7 @@ module.exports = {
99103
const tnodes = split.gather([1], false);
100104
const nodes = json_key_attributes_to_dataframe(tnodes);
101105
const edges = json_aos_to_dataframe(
102-
tedges, ['key', 'source', 'target'], [new Utf8String, new Int32, new Int32]);
106+
tedges, ['key', 'source', 'target'], [new Utf8String, new Int64, new Int64]);
103107
let optionsArr = {};
104108
optionsArr['type'] = Series.new(toptions.getJSONObject('.type'));
105109
optionsArr['multi'] = Series.new(toptions.getJSONObject('.multi'));
@@ -129,7 +133,7 @@ module.exports = {
129133
const tnodes = split.gather([1], false);
130134
const tags = json_aos_to_dataframe(ttags, ['key', 'image'], [new Utf8String, new Utf8String]);
131135
const clusters = json_aos_to_dataframe(
132-
tclusters, ['key', 'color', 'clusterLabel'], [new Int32, new Utf8String, new Utf8String]);
136+
tclusters, ['key', 'color', 'clusterLabel'], [new Int64, new Utf8String, new Utf8String]);
133137
const nodes =
134138
json_aos_to_dataframe(tnodes, ['key', 'label', 'tag', 'URL', 'cluster', 'x', 'y', 'score'], [
135139
new Utf8String,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2022, NVIDIA CORPORATION.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const schema = {
18+
graphology: {
19+
description: 'The graphology api provides GPU acceleration of graphology datasets.',
20+
schema: {
21+
read_json: {
22+
filename: 'A URI to a graphology json dataset file.',
23+
result: `Causes the node-rapids backend to attempt to load the json object specified
24+
by :filename. The GPU will attempt to parse the json file asynchronously and will
25+
return OK/ Not Found/ or Fail based on the file status.
26+
If the load is successful, four tables will be created in the node-rapids backend:
27+
nodes, edges, clusters, and tags. The root objects in the json target must match
28+
these names and order.`,
29+
returns: 'Result OK/Not Found/Fail'
30+
},
31+
read_large_demo: {
32+
filename:
33+
'A URI to a graphology json dataset file matching the sigma.js/examples/large-demos spec.',
34+
result: `Produces the same result as 'read_json'.
35+
If the load is successful, three tables will be created in the node-rapids backend:
36+
nodes, edges, and options.`,
37+
returns: 'Result OK/Not Found/Fail'
38+
},
39+
list_tables: {returns: 'Tables that are available presently in GPU memory.'},
40+
get_table: {
41+
':table':
42+
{table: 'The name of the table that has been allocated previously into GPU memory.'}
43+
},
44+
get_column: {':table': {':column': {table: 'The table name', column: 'The column name'}}},
45+
nodes: {
46+
returns:
47+
'Returns the existing nodes table after applying normalization functions for sigma.js'
48+
},
49+
nodes: {bounds: {returns: 'Returns the x and y bounds to be used in rendering.'}},
50+
edges: {return: 'Returns the existing edges table after applying normalization for sigma.js'}
51+
}
52+
}
53+
};
54+
55+
module.exports = schema;

0 commit comments

Comments
 (0)